天天看點

BOOST 線程完全攻略

BOOST 線程完全攻略 - 基礎篇

jackyhwei 釋出于 2010-11-01 09:26
BOOST 線程完全攻略
來自:CSDN部落格

boost标準stl庫的出現,讓我眼前一亮,boost所推行的簡潔代碼概念和模闆概念,讓我有了清風撲面的感覺,本文将介紹如何使用boost::thread來取代不太安全的MFC線程程式設計。 TAG: boost   多線程  

C++多線程開發是一個複雜的事情,mfc下提供了CWinThread類,和AfxBeginThread等等函數,但是在使用中會遇到很多麻煩 事情,例如線程之間參數傳遞的問題,我們一般都是把參數new一個結構體,傳遞給子線程,然後釋放的工作交給了子線程,這樣不可避免會有記憶體洩漏的危險, 例如線程關閉的問題,我們一般用WaitForSingleObject來完成線程關閉工作,但是這個函數并不一定保證線程能收到要關閉的信号,這樣父親 已經退出工作了,子線程還在工作,程式也會有潛在的危險。

是以我已經慢慢不再用這套線程機制了,boost标準stl庫的出現,讓我眼前一亮,boost所推行的簡潔代碼概念和模闆概念,讓我有了清風撲面的感覺,本文将介紹如何使用boost::thread來取代不太安全的MFC線程程式設計。

本文所牽涉到的源碼下載下傳位址:http://download.csdn.net/source/618903

基礎篇主要是彙集和轉載一些已有網文,讓初學者入門boost::thread.

一. 安裝 原位址http://www.douban.com/group/topic/2494650/

1.下載下傳boost_1_34_1壓縮檔案,解壓縮到d:\boost_1_34_1\目錄下

2.編譯bjam

從vs2005的工具菜單進入指令提示視窗(一定要從這進),cd到d:\boost_1_34_1\tools\jam\src下執行 build.bat,會在d:\boost_1_34_1\tools\jam\src\bin.ntx86\產生bjam.exe,將bjam.exe 複製到d:\boost_1_34_1\下。

3.設定編譯環境

修改user-config.jam (d:\boost_1_34_1\tools\build\v2\user-config.jam) 的MSVC configuration

# MSVC configuration

# Configure msvc (default version, searched in standard location

# and PATH).

# using msvc ;

using msvc : 8.0 : : <compileflags>/wd4819 <compileflags>/D_CRT_SECURE_NO_DEPRECATE <compileflags>/D_SCL_SECURE_NO_DEPRECATE <compileflags>/D_SECURE_SCL=0 ;

4.編譯boost

將目錄移至d:\boost_1_34_1\下執行

bjam --without-python --toolset=msvc-8.0 --prefix=d:\boost install

參數說明

--without-python 表示不使用 python

--toolset : 所使用compiler,Visual Studio 2005為msvc-8.0

--prefix:指定編譯後library的安裝目錄

這一步要花很長時間(大約50分鐘)

5.設定vs2005環境

Tools -> Options -> Projects and Solutions -> VC++ Directories

在Library files加上d:\boost\lib

在Include files加上d:\boost\include\boost-1_34_1

二.boost::thread入門 原文http://www.stlchina.org/twiki/bin/view.pl/Main/BoostThread

1 建立線程

就像std::fstream類就代表一個檔案一樣,boost::thread類就代表一個可執行的線程。預設構造函數建立一個代表目前執行線程的實 例。一個重載的構造函數以一個不需任何參數的函數對象作為參數,并且沒有傳回值。這個構造函數建立一個新的可執行線程,它調用了那個函數對象。

起先,大家認為傳統C建立線程的方法似乎比這樣的設計更有用,因為C建立線程的時候會傳入一個void*指針,通過這種方法就可以傳入資料。然而,由于 Boost線程庫是使用函數對象來代替函數指針,那麼函數對象本身就可以攜帶線程所需的資料。這種方法更具靈活性,也是類型安全(type-safe) 的。當和Boost.Bind這樣的功能庫一起使用時,這樣的方法就可以讓你傳遞任意數量的資料給建立的線程。

目前,由Boost線程庫建立的線程對象功能還不是很強大。事實上它隻能做兩項操作。線程對象可以友善使用==和!=進行比較來确定它們是否是代表同一個 線程;你還可以調用boost::thread::join來等待線程執行完畢。其他一些線程庫可以讓你對線程做一些其他操作(比如設定優先級,甚至是取 消線程)。然而,由于要在普遍适用(portable)的接口中加入這些操作不是簡單的事,目前仍在讨論如何将這些操組加入到Boost線程庫中。

Listing1展示了boost::thread類的一個最簡單的用法。 建立的線程隻是簡單的在std::out上列印“hello,world”,main函數在它執行完畢之後結束。

  1. #include <boost/thread/thread.hpp> 
  2. #include <iostream> 
  3. void hello() 
  4.         std::cout << 
  5.         "Hello world, I'm a thread!" 
  6.         << std::endl; 
  7. int main(int argc, char* argv[]) 
  8.         boost::thread thrd(&hello); 
  9.         thrd.join(); 
  10.         return 0; 

2 互斥體

任何寫過多線程程式的人都知道避免不同線程同時通路共享區域的重要性。如果一個線程要改變共享區域中某個資料,而與此同時另一線程正在讀這個資料,那麼結 果将是未定義的。為了避免這種情況的發生就要使用一些特殊的原始類型和操作。其中最基本的就是互斥體(mutex,mutual exclusion的縮寫)。一個互斥體一次隻允許一個線程通路共享區。當一個線程想要通路共享區時,首先要做的就是鎖住(lock)互斥體。如果其他的 線程已經鎖住了互斥體,那麼就必須先等那個線程将互斥體解鎖,這樣就保證了同一時刻隻有一個線程能通路共享區域。

互斥體的概念有不少變種。Boost線程庫支援兩大類互斥體,包括簡單互斥體(simple mutex)和遞歸互斥體(recursive mutex)。如果同一個線程對互斥體上了兩次鎖,就會發生死鎖(deadlock),也就是說所有的等待解鎖的線程将一直等下去。有了遞歸互斥體,單個 線程就可以對互斥體多次上鎖,當然也必須解鎖同樣次數來保證其他線程可以對這個互斥體上鎖。

在這兩大類互斥體中,對于線程如何上鎖還有多個變種。一個線程可以有三種方法來對一個互斥體加鎖:

一直等到沒有其他線程對互斥體加鎖。

如果有其他互斥體已經對互斥體加鎖就立即傳回。

一直等到沒有其他線程互斥體加鎖,直到逾時。

似乎最佳的互斥體類型是遞歸互斥體,它可以使用所有三種上鎖形式。然而每一個變種都是有代價的。是以Boost線程庫允許你根據不同的需要使用最有效率的互斥體類型。Boost線程庫提供了6中互斥體類型,下面是按照效率進行排序:

boost::mutex,

boost::try_mutex,

boost::timed_mutex,

boost::recursive_mutex,

boost::recursive_try_mutex, 

boost::recursive_timed_mutex

如果互斥體上鎖之後沒有解鎖就會發生死鎖。這是一個很普遍的錯誤,Boost線程庫就是要将其變成不可能(至少時很困難)。直接對互斥體上鎖和解鎖對于 Boost線程庫的使用者來說是不可能的。mutex類通過teypdef定義在RAII中實作的類型來實作互斥體的上鎖和解鎖。這也就是大家知道的 Scope Lock模式。為了構造這些類型,要傳入一個互斥體的引用。構造函數對互斥體加鎖,析構函數對互斥體解鎖。C++保證了析構函數一定會被調用,是以即使是 有異常抛出,互斥體也總是會被正确的解鎖。

這種方法保證正确的使用互斥體。然而,有一點必須注意:盡管Scope Lock模式可以保證互斥體被解鎖,但是它并沒有保證在異常抛出之後貢獻資源仍是可用的。是以就像執行單線程程式一樣,必須保證異常不會導緻程式狀态異 常。另外,這個已經上鎖的對象不能傳遞給另一個線程,因為它們維護的狀态并沒有禁止這樣做。

List2給出了一個使用boost::mutex的最簡單的例子。例子中共建立了兩個新的線程,每個線程都有10次循環,在std::cout上列印出 線程id和目前循環的次數,而main函數等待這兩個線程執行完才結束。std::cout就是共享資源,是以每一個線程都使用一個全局互斥體來保證同時 隻有一個線程能向它寫入。

許多讀者可能已經注意到List2中傳遞資料給線程還必須的手工寫一個函數。盡管這個例子很簡單,如果每一次都要寫這樣的代碼實在是讓人厭煩的事。别急, 有一種簡單的解決辦法。函數庫允許你通過将另一個函數綁定,并傳入調用時需要的資料來建立一個新的函數。 List3向你展示了如何使用Boost.Bind庫來簡化List2中的代碼,這樣就不必手工寫這些函數對象了。

  1. #include <boost/thread/thread.hpp> 
  2. #include <boost/thread/mutex.hpp> 
  3. #include <iostream> 
  4. boost::mutex io_mutex; 
  5. struct count 
  6.         count(int id) : id(id) { } 
  7.         void operator()() 
  8.         { 
  9.                 for (int i = 0; i < 10; ++i) 
  10.                 { 
  11.                         boost::mutex::scoped_lock 
  12.                         lock(io_mutex); 
  13.                         std::cout << id << ": " 
  14.                         << i << std::endl; 
  15.                 } 
  16.         } 
  17.         int id; 
  18. }; 
  19. int main(int argc, char* argv[]) 
  20.         boost::thread thrd1(count(1)); 
  21.         boost::thread thrd2(count(2)); 
  22.         thrd1.join(); 
  23.         thrd2.join(); 
  24.         return 0; 
除了使用Boost.Bind來簡化建立線程攜帶資料,避免使用函數對象
  1. #include <boost/thread/thread.hpp> 
  2. #include <boost/thread/mutex.hpp> 
  3. #include <boost/bind.hpp> 
  4. #include <iostream> 
  5. boost::mutex io_mutex; 
  6. void count(int id) 
  7.         for (int i = 0; i < 10; ++i) 
  8.         { 
  9.                 boost::mutex::scoped_lock 
  10.                 lock(io_mutex); 
  11.                 std::cout << id << ": " << 
  12.                 i << std::endl; 
  13.         } 
  14. int main(int argc, char* argv[]) 
  15.         boost::thread thrd1( 
  16.         boost::bind(&count, 1)); 
  17.         boost::thread thrd2( 
  18.         boost::bind(&count, 2)); 
  19.         thrd1.join(); 
  20.         thrd2.join(); 
  21.         return 0; 

3 條件變量

有的時候僅僅依靠鎖住共享資源來使用它是不夠的。有時候共享資源隻有某些狀态的時候才能夠使用。比方說,某個線程如果要從堆棧中讀取資料,那麼如果棧中沒 有資料就必須等待資料被壓棧。這種情況下的同步使用互斥體是不夠的。另一種同步的方式--條件變量,就可以使用在這種情況下。

條件變量的使用總是和互斥體及共享資源聯系在一起的。線程首先鎖住互斥體,然後檢驗共享資源的狀态是否處于可使用的狀态。如果不是,那麼線程就要等待條件 變量。要指向這樣的操作就必須在等待的時候将互斥體解鎖,以便其他線程可以通路共享資源并改變其狀态。它還得保證從等到得線程傳回時互斥體是被上鎖得。當 另一個線程改變了共享資源的狀态時,它就要通知正在等待條件變量得線程,并将之傳回等待的線程。

List4是一個使用了boost::condition的簡單例子。有一個實作了有界緩存區的類和一個固定大小的先進先出的容器。由于使用了互斥體 boost::mutex,這個緩存區是線程安全的。put和get使用條件變量來保證線程等待完成操作所必須的狀态。有兩個線程被建立,一個在 buffer中放入100個整數,另一個将它們從buffer中取出。這個有界的緩存一次隻能存放10個整數,是以這兩個線程必須周期性的等待另一個線 程。為了驗證這一點,put和get在std::cout中輸出診斷語句。最後,當兩個線程結束後,main函數也就執行完畢了。

  1. #include <boost/thread/thread.hpp> 
  2. #include <boost/thread/mutex.hpp> 
  3. #include <boost/thread/condition.hpp> 
  4. #include <iostream> 
  5. const int BUF_SIZE = 10; 
  6. const int ITERS = 100; 
  7. boost::mutex io_mutex; 
  8. class buffer 
  9.         public: 
  10.         typedef boost::mutex::scoped_lock 
  11.         scoped_lock; 
  12.         buffer() 
  13.         : p(0), c(0), full(0) 
  14.         { 
  15.         } 
  16.         void put(int m) 
  17.         { 
  18.                 scoped_lock lock(mutex); 
  19.                 if (full == BUF_SIZE) 
  20.                 { 
  21.                         { 
  22.                                 boost::mutex::scoped_lock 
  23.                                 lock(io_mutex); 
  24.                                 std::cout << 
  25.                                 "Buffer is full. Waiting..." 
  26.                                 << std::endl; 
  27.                         } 
  28.                         while (full == BUF_SIZE) 
  29.                         cond.wait(lock); 
  30.                 } 
  31.                 buf[p] = m; 
  32.                 p = (p+1) % BUF_SIZE; 
  33.                 ++full; 
  34.                 cond.notify_one(); 
  35.         } 
  36.         int get() 
  37.         { 
  38.                 scoped_lock lk(mutex); 
  39.                 if (full == 0) 
  40.                 { 
  41.                         { 
  42.                                 boost::mutex::scoped_lock 
  43.                                 lock(io_mutex); 
  44.                                 std::cout << 
  45.                                 "Buffer is empty. Waiting..." 
  46.                                 << std::endl; 
  47.                         } 
  48.                         while (full == 0) 
  49.                         cond.wait(lk); 
  50.                 } 
  51.                 int i = buf[c]; 
  52.                 c = (c+1) % BUF_SIZE; 
  53.                 --full; 
  54.                 cond.notify_one(); 
  55.                 return i; 
  56.         } 
  57.         private: 
  58.         boost::mutex mutex; 
  59.         boost::condition cond; 
  60.         unsigned int p, c, full; 
  61.         int buf[BUF_SIZE]; 
  62. }; 
  63. buffer buf; 
  64. void writer() 
  65.         for (int n = 0; n < ITERS; ++n) 
  66.         { 
  67.                 { 
  68.                         boost::mutex::scoped_lock 
  69.                         lock(io_mutex); 
  70.                         std::cout << "sending: " 
  71.                         << n << std::endl; 
  72.                 } 
  73.                 buf.put(n); 
  74.         } 
  75. void reader() 
  76.         for (int x = 0; x < ITERS; ++x) 
  77.         { 
  78.                 int n = buf.get(); 
  79.                 { 
  80.                         boost::mutex::scoped_lock 
  81.                         lock(io_mutex); 
  82.                         std::cout << "received: " 
  83.                         << n << std::endl; 
  84.                 } 
  85.         } 
  86. int main(int argc, char* argv[]) 
  87.         boost::thread thrd1(&reader); 
  88.         boost::thread thrd2(&writer); 
  89.         thrd1.join(); 
  90.         thrd2.join(); 
  91.         return 0; 

4.線程局部存儲

大多數函數都不是可重入的。這也就是說在某一個線程已經調用了一個函數時,如果你再調用同一個函數,那麼這樣是不安全的。一個不可重入的函數通過連續的調 用來儲存靜态變量或者是傳回一個指向靜态資料的指針。 舉例來說,std::strtok就是不可重入的,因為它使用靜态變量來儲存要被分割成符号的字元串。

有兩種方法可以讓不可重用的函數變成可重用的函數。第一種方法就是改變接口,用指針或引用代替原先使用靜态資料的地方。比方說,POSIX定義了 strok_r,std::strtok中的一個可重入的變量,它用一個額外的char**參數來代替靜态資料。這種方法很簡單,而且提供了可能的最佳效 果。但是這樣必須改變公共接口,也就意味着必須改代碼。另一種方法不用改變公有接口,而是用本地存儲線程(thread local storage)來代替靜态資料(有時也被成為特殊線程存儲,thread-specific storage)。

Boost線程庫提供了智能指針boost::thread_specific_ptr來通路本地存儲線程。每一個線程第一次使用這個智能指針的執行個體時, 它的初值是NULL,是以必須要先檢查這個它的隻是否為空,并且為它指派。Boost線程庫保證本地存儲線程中儲存的資料會線上程結束後被清除。

List5是一個使用boost::thread_specific_ptr的簡單例子。其中建立了兩個線程來初始化本地存儲線程,并有10次循環,每一 次都會增加智能指針指向的值,并将其輸出到std::cout上(由于std::cout是一個共享資源,是以通過互斥體進行同步)。main線程等待這 兩個線程結束後就退出。從這個例子輸出可以明白的看出每個線程都處理屬于自己的資料執行個體,盡管它們都是使用同一個 boost::thread_specific_ptr。

  1. #include <boost/thread/thread.hpp> 
  2. #include <boost/thread/mutex.hpp> 
  3. #include <boost/thread/tss.hpp> 
  4. #include <iostream> 
  5. boost::mutex io_mutex; 
  6. boost::thread_specific_ptr<int> ptr; 
  7. struct count 
  8.         count(int id) : id(id) { } 
  9.         void operator()() 
  10.         { 
  11.                 if (ptr.get() == 0) 
  12.                 ptr.reset(new int(0)); 
  13.                 for (int i = 0; i < 10; ++i) 
  14.                 { 
  15.                         (*ptr)++; 
  16.                         boost::mutex::scoped_lock 
  17.                         lock(io_mutex); 
  18.                         std::cout << id << ": " 
  19.                         << *ptr << std::endl; 
  20.                 } 
  21.         } 
  22.         int id; 
  23. }; 
  24. int main(int argc, char* argv[]) 
  25.         boost::thread thrd1(count(1)); 
  26.         boost::thread thrd2(count(2)); 
  27.         thrd1.join(); 
  28.         thrd2.join(); 
  29.         return 0; 

5 僅運作一次的例程

還有一個問題沒有解決:如何使得初始化工作(比如說構造函數)也是線程安全的。比方說,如果一個引用程式要産生唯一的全局的對象,由于執行個體化順序的問題, 某個函數會被調用來傳回一個靜态的對象,它必須保證第一次被調用時就産生這個靜态的對象。這裡的問題就是如果多個線程同時調用了這個函數,那麼這個靜态對 象的構造函數就會被調用多次,這樣錯誤産生了。

解決這個問題的方法就是所謂的“一次實作”(once routine)。“一次實作”在一個應用程式隻能執行一次。如果多個線程想同時執行這個操作,那麼真正執行的隻有一個,而其他線程必須等這個操作結束。 為了保證它隻被執行一次,這個routine由另一個函數間接的調用,而這個函數傳給它一個指針以及一個标志着這個routine是否已經被調用的特殊标 志。這個标志是以靜态的方式初始化的,這也就保證了它在編譯期間就被初始化而不是運作時。是以也就沒有多個線程同時将它初始化的問題了。Boost線程庫 提供了boost::call_once來支援“一次實作”,并且定義了一個标志boost::once_flag及一個初始化這個标志的宏 BOOST_ONCE_INIT。

List6是一個使用了boost::call_once的例子。其中定義了一個靜态的全局整數,初始值為0;還有一個由BOOST_ONCE_INIT 初始化的靜态boost::once_flag執行個體。main函數建立了兩個線程,它們都想通過傳入一個函數調用boost::call_once來初始 化這個全局的整數,這個函數是将它加1。main函數等待着兩個線程結束,并将最後的結果輸出的到std::cout。由最後的結果可以看出這個操作确實 隻被執行了一次,因為它的值是1。

  1. #include <boost/thread/thread.hpp> 
  2. #include <boost/thread/once.hpp> 
  3. #include <iostream> 
  4. int i = 0; 
  5. boost::once_flag flag = 
  6. BOOST_ONCE_INIT; 
  7. void init() 
  8.         ++i; 
  9. void thread() 
  10.         boost::call_once(&init, flag); 
  11. int main(int argc, char* argv[]) 
  12.         boost::thread thrd1(thread); 
  13.         boost::thread thrd2(thread); 
  14.         thrd1.join(); 
  15.         thrd2.join(); 
  16.         std::cout << i << std::endl; 
  17.         return 0; 

6 Boost線程庫的未來

Boost線程庫正在計劃加入一些新特性。其中包括boost::read_write_mutex,它可以讓多個線程同時從共享區中讀取資料,但是一次 隻可能有一個線程向共享區寫入資料;boost::thread_barrier,它使得一組線程處于等待狀态,知道所有得線程都都進入了屏障 區;boost::thread_pool,他允許執行一些小的routine而不必每一都要建立或是銷毀一個線程。

Boost線程庫已經作為标準中的類庫技術報告中的附件送出給C++标準委員會,它的出現也為下一版C++标準吹響了第一聲号角。委員會成員對Boost 線程庫的初稿給予了很高的評價,當然他們還會考慮其他的多線程庫。他們對在C++标準中加入對多線程的支援非常感興趣。從這一點上也可以看出,多線程在 C++中的前途一片光明。

7 參考資料:

The Boost.Threads Library by Bill Kempf

Visit the Boost website at <http://www.boost.org>.

(IamNieo)