C++ 的多執行序程式開發 Thread:基本使用


在很久以前,Heresy 有寫過一系列《簡易的程式平行化方法-OpenMP》的文章,算是一種很簡單的平行化程式的開發方法,對於要把迴圈平行化,算是一種最簡單的方法了~而後來 Heresy 也有介紹過 nVIDIA CUDA 或者 OpenCL 這類、以大量平行化為目標的 GPGPU 程式開發架構。

Data parallelism 和 Task parallelism

實際上,在程式開發時,所謂的「平行化」(parallelism),大致上可以分為「Data parallelism」(資料平行化、維基百科和「Task parallelism」(工作平行化、維基百科)兩大概念。

其中,「data parallelism」的使用最簡單的例子,就是把迴圈平行化、分成好幾個 thread(執行序)來同時進行計算;而在這樣執行時,每個 thread 的所做的事情其實都是相同的,只有計算的資料不同而已~像是 OpenMP 裡面的「parallel for」(參考)、OpenCL 的「Data-Parallel Execution Model」、或是 nVIDIA CUDA 這類的平行化方法,基本上都是屬於「data parallelism」的類型。

但是有的時候,可能會是想要讓幾個很複雜、而且不相同的計算同時進行,這時候就沒辦法使用「data parallelism」的概念、而是要使用「task parallelism」的方法了~他的基本概念,就是在程式裡面產生額外的 thread、來進行不同的、獨立的計算;像是 OpenMP 的「section」(參考),就是屬於「task parallelism」的方法。

C++11 Thread

雖然 OpenMP 已經有提供了 task parallelism 的功能,不過實際上它所提供的功能相當簡單、也不無法做進一步的控制,所以基本上應該也只能適用於較簡單的例子。而這邊 Heresy 要來介紹的,則是一個功能比較完整的 thread 控制的函示庫,那就是 C++11 的 STL 新加入的「Thread」(以下稱為「STL Thread」,官方文件MSDN)!

不過,雖然 STL Thread 是 C++11 標準函式庫的一部分,但是要注意的是,由於 C++11 還算是一個很新的標準,並非所有編譯器都有支援;像是 Visual C++ 2010 就還不支援、要等到下一代的 Visual Studio 2012 才有支援。所以如果是要在 MSVC10 這種還不支援 STL Thread 的開發環境下使用的話,可以考慮使用 Boost C++ Libraries 所提供的 Thread 函式庫(官方文件),他基本上是和 STL Thread 相同的(最大的差異只在於 namespace)(gcc 4.6 對 STL thread 的支援性似乎也還不是很好)

 

基本使用

如果只是要產生一個新的執行序來執行額外的程式的話,STL Thread 的基本使用其實相當簡單,大致上如下:

#include <iostream>
#include <thread>

using namespace std;

void test_func()
{
  // do something
  double dSum = 0;
  for( int i = 0; i < 10000; ++ i )
    for( int j = 0; j < 10000; ++ j )
      dSum += i*j;

  cout << "Thread: " << dSum << endl;
}

int main( int argc, char** argv )
{
  // execute thread
  thread mThread( test_func );

  // do somthing
  cout << "main thread" << endl;

  // wait the thread stop
  mThread.join();

  return 0;
}

首先,STL Thread 的 header file 是 <thread>,在使用前必須要先 include 這個檔案。

而要產生新的 thread,基本上就是取去建立一個新的 std::thread 的物件,在這邊就是 mThread;而在建立 std::thread 的物件的時候,可以直接把一個可以呼叫的物件(callable object、一般是現成的 function 或是 function object)當作參數傳進去,這樣在 mThread 這個物件被建立出來的時候,系統就會產稱一個新的執行序、去執行所指定的 function object 了~而在這邊,就是 test_func() 這個函式。

而當新的執行序開始執行後,雖然電腦會開始執行 test_func() 裡面的計算,但是同時,他也會繼續執行下面的指令,在這邊就是透過 cout 輸出「main thread」這個字串。

可以看到,在這邊的範例裡,Heresy 是刻意把 test_func() 裡的計算寫得很複雜、用來拖時間。

main thread
Thread: 2.4995e+015

最後面去呼叫 mThreadjoin() 這個函式,則是用來告訴編譯器,在這邊要等 mThread 的計算工作完成後、才能繼續做下去;如此一來,可以避免 mThread 明明還在進行計算,但是主程式卻已經結束的問題。而如果之後的程式有要用到其他執行序的計算結果的話,也是要記得加上 join(),才能確定所需要的計算已經結束了。

而如果要執行的 function object 是需要參數的話,也可以直接在建立 std::thread 物件的時候,直接把參數附加在建構子裡。下面就是一個例子:

#include <iostream>
#include <thread>

using namespace std;

void test_func2( int i )
{
  cout << i << endl;
}

int main( int argc, char** argv )
{
  thread mThread( test_func2, 10 );
  mThread.join();

  return 0;
}

不過要注意的一點是,在把 callable object 傳遞給 STL Thread 開啟一個新的 thread 的時候,他會是採用複製的方法,把傳入的物件複製一份來用;所以如果在裡面有修改道本身的資料的話,就需要使用 std::ref() 來產生物件的參考、然後再傳進去。下面就是一個例子:

#include <iostream>
#include <thread>

using namespace std;

class funcObj
{
public:
  int iData;

  funcObj()
  {
    iData = 0;
  }

  void operator()()
  {
    ++iData;
  }
};

int main( int argc, char** argv )
{
  funcObj co;

  // copy
  thread mThread1( co );
  mThread1.join();
  cout << co.iData << endl;

  // reference
  thread mThread2( ref( co ) );
  mThread2.join();
  cout << co.iData << endl;

  return 0;
}

這邊的 funcObj 就是一個有 call operator(operator()())的類別,他被呼叫的時候,會把內部的計數器(iData)的值加 1。而在主程式裡面,第一次使用 STL Thread 執行的時候,是直接把 funcObj 的物件(co)傳進去;這時候他會在內部複製一份來執行,所以當 mThread1 執行結束後,co 裡的 iData 的值並不會改變。

而當第二次執行的時候,由於傳進到 STL thread 建構子的物件是 ref( co ),所以實際上 mThread2 所執行的會是 co 這個 funcObj 物件的參考;也因此,co.iData 就會在 mThread2 裡被修改到,等到結束後,他的值就會變成 1 了~

 

其他功能

上面基本上就是 STL Thread 最基本的使用了~透過 std::thread 這個物件,基本上是可以相當簡單地開啟一個新的執行序來處理額外的計算,然後在目前的執行序、同時繼續做其他的計算的;而實務上,有需要的話,也是可以開許多個執行序來用的~

接下來這邊,則是一些也算是基礎的其他功能。

  • thread::hardware_concurrency()

    hardware_concurrency()std::thread 的 static member function,可以用來取得在硬體層面上可以同時執行的執行序的數量,基本上可以視為處理器的核心數目;不過實際上這只是估計值,如果無法判斷時,值會是 0。(參考

  • this_thread

    this_thread 是 STL thread 裡的一個特別的 namespace,底下提供 get_id()yield()sleep_until()sleep_for() 四個函式可以呼叫,都是針對目前的執行序進行操作的。

    其中,get_id() 可以用來取得目前的執行序的 id(型別是 thread::id);另一方面,也可以透過 std::thread 的物件的 get_id() 這個 member function 來取得(例如:mThread.get_id())。這個功能主要是可以用來識別不同的執行序,有的時候是用的到的。

    sleep_for()sleep_until() 則是用來讓目前的執行序暫時停下來的,前者是停止一段指定的時間、後者則是設定一個絕對時間、讓執行序在指定的時間再繼續執行;而時間的參數,則是要使用 std::chronoMSDN)的 duration範例)和 time_point範例)這兩種型別的時間資料。

    yield() 是暫時放棄一段 CPU 時間、讓給其他執行序使用的;這個應該算是比較進階的使用了,在這邊暫時跳過,之後有機會再整理。


這篇算是 STL Thread 最簡單的使用了。接下來,應該會再花點時間、整理一下中斷一個執行序、以及多執行序間同步的處理問題。不過,就要再等一段時間了。


廣告

對「C++ 的多執行序程式開發 Thread:基本使用」的想法

  1. 謝謝 Heresy 的文章,最近開始研究 multi thread,以下幫忙補充 sleep_for()、sleep_until() 的範例

    #define _CRT_SECURE_NO_WARNINGS

    void sleep_for_test() {
    int i = 0;
    while (i < 10)
    {
    // Print Thread ID and Counter i
    cout << this_thread::get_id() << " :: " << i++ << endl;

    // Sleep this thread for 200 MilliSeconds
    // std::chrono::nanoseconds / std::chrono::microseconds / std::chrono::milliseconds
    this_thread::sleep_for(chrono::milliseconds(200));
    }
    }

    // Print Current Time
    const string currentDateTime() {
    time_t now = time(0);
    struct tm tstruct;
    char buf[80];
    tstruct = *localtime(&now);
    // Visit http://en.cppreference.com/w/cpp/chrono/c/strftime
    // for more information about date/time format
    strftime(buf, sizeof(buf), "%Y-%m-%d.%X", &tstruct);

    return buf;
    }

    void sleep_until_test() {

    // Print Current Time
    cout << "currentDateTime()=" << currentDateTime() << endl;

    // create a time point pointing to 10 second in future
    chrono::system_clock::time_point timePoint = chrono::system_clock::now() + chrono::seconds(10);

    // Sleep Till specified time point
    // Accepts std::chrono::system_clock::time_point as argument
    this_thread::sleep_until(timePoint);

    // Print Current Time
    cout << "currentDateTime()=" << currentDateTime() << endl;
    }

    thread mThread3(sleep_for_test);
    mThread3.join();

    thread mThread4(sleep_until_test);
    mThread4.join();

  2. 您好,敝人最近也在研究C++11的std::thread(我看的書是《C++ Concurrency In Action》)。因為我是直接學C++11的std::thread,所以沒跟以前C用的OpenMP或是MPI比較過;不知道大大有沒有比較過他們之間的效能呢? 不知道C++11的std是否能完全取代MPI呢?

    • Heresy 不確定你的目的是什麼,但是就 Heresy 的理解,MPI 的目的、功能和 OpenMP 以及 Thread Library 應該是完全不同的。

      另外,OpenMP 和 std::thread 在功能、目標面上,也有一定程度的差異。

      • 抱歉問題不清楚,我想問的是關於在工作站之類的大型電腦上做科學運算的問題哎~ 把用C++11本身的STD寫的程式(不論是std::thread或std::future),利用PBS丟到Cluster上運算時,它會自動把工作分佈到不同的Node去嗎?

        • STD 的 thread 函式庫或 OpenMP 基本上都是 shared memory 架構下的平行化方法,只能針對單台電腦使用,並不能直接套用在電腦叢集上。

    • 如果你是比較 Boost.Thread 和 std::thread 的話,兩者只是在執行序的管理上有不同的實作(介面也有點不一樣)。
      但是一般來說,除非 thread 的數量多到一定程度,否則對於效能的影響應該不會太大才對。

  3. 請問我用vs2010的#include 與vs2013用的#include ,有什麼不同? 用這個方法能夠讓我使用的多台kinect讀取速度更快嗎?

    • 抱歉,由於你的留言中有特殊字元,所以無法判斷出你的問題到底是要比較哪兩個函示庫。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.