一、概述
線程是在同一程式同一時間內允許執行不同函數的離散處理隊列,這使得在一個長時間進行某種特殊運算的函數在執行時不阻礙其他的函數時變得十分重要。線程實際上允許同時執行兩種函數,而這兩者不必相互等待。
一旦一個應用程式啟動,它僅包含一個預設線程。此線程執行main()函數。在main()中被調用的函數則按這個線程的上下文順序地執行,這樣的程式稱為單線程程式。
反之,那些建立新的線程的程式就是多線程程式。他們不僅可以在同一時間執行多個函數,而且這在如今多核盛行的時代顯得尤為重要。既然多核允許同時執行多個函數,這就使得對開發人員相應地使用這種處理能力提出了要求。然而線程一直被用來當並發地執行多個函數,開發人員現在不得不仔細地構建應用來支援這種並發。多線程編程知識也因此在多核系統時代變得越來越重要。
本章介紹的是C++ Boost庫Boost.Thread,它可以開發獨立於平台的多線程應用程式。
二、線程管理
這個庫中最重要的一個類就是boost::thread,它在boost/thread.hpp裡定義,用來建立一個新線程。下面的樣本來說明如何運用它。
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } void thread() { for (int i = 0; i < 5; ++i) { wait(1); std::cout << i << std::endl; } } int main() { boost::thread t(thread); t.join(); }
建立線程裡執行的那個函數的名稱被傳遞到 boost::thread 的建構函式。一旦上述樣本中的變數t被建立,該thread函數就在其所線上程中被立即執行,同時在main()裡也並發地執行該thread。
樣本中,為了防止程式終止,就需要對建立線程調用join方法。join方法是一個阻塞調用:它可以暫停當前線程,直到調用join的線程運行結束。這就使得main函數一直會等待到thread運行結束。
正如上面例子中看到的,一個特定的線程可以通過諸如t的變數訪問,通過這個變數等待著它的使用join方法終止。 但是,即使t越界或者析構了,該線程也將繼續執行。一個線程總是在一開始就綁定到一個類型為 boost::thread 的變數,但是一旦建立,就不在取決於它。 甚至還存在著一個叫detach的方法,允許類型為 boost::thread 的變數從它對應的線程裡分離。當然,像 join的方法之後也就不能被調用,因為這個變數不再是一個有效線程。
任何一個函數內可以做的事情也可以在一個線程內完成。所以,一個線程只不過是一個函數,除了它是同時執行的。在上述例子中,使用一個迴圈把5個數字寫入標準輸出資料流。為了減緩輸出,每一個迴圈中調用wait函數讓執行延遲了一秒。wait可以調用一個名為sleep的函數,這個函數也來自於 Boost.Thread,位於 boost::this_thread 命名空間內。
sleep()可以在預計的一段時間或一個特定的時間點後才讓線程繼續執行。通過傳遞一個類型為 boost::posix_time::seconds 的對象,在這個例子裡我們指定了一段時間。 boost::posix_time::seconds 來自於 Boost.DateTime 庫,它被 Boost.Thread 用來管理和處理時間的資料。
雖然前面的例子說明了如何等待一個不同的線程,但下面的例子示範了如何通過所謂的中斷點讓一個線程中斷。
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } void thread() { try { for (int i = 0; i < 5; ++i) { wait(1); std::cout << i << std::endl; } } catch (boost::thread_interrupted&) { } } int main() { boost::thread t(thread); wait(3); t.interrupt(); t.join(); }
在一個線程對象上調用 interrupt() 會中斷相應的線程。 在這方面,中斷意味著一個類型為 boost::thread_interrupted 的異常,它會在這個線程中拋出。 然後這隻有線上程達到中斷點時才會發生。
如果給定的線程不包含任何中斷點,簡單調用interrupt就不會起作用。 每當一個線程中斷點,它就會檢查interrupt是否被調用過。只有被調用過了, boost::thread_interrupted 異常才會相應地拋出。
Boost.Thread定義了一系列的中斷點,例如sleep() 函數,由於sleep() 在這個例子裡被調用了五次,該線程就檢查了五次它是否應該被中斷。然而sleep()之間的調用,卻不能使線程中斷。
一旦該程式被執行,它只會列印三個數字到標準輸出資料流。這是由於在main裡3秒後調用 interrupt()方法。 因此,相應的線程被中斷,並拋出一個 boost::thread_interrupted 異常。這個異常線上程內也被正確地捕獲,catch 處理是空的。由於thread()函數在處理常式後返回,線程也被終止。這反過來也將終止整個程式,因為 main() 等待該線程使用join終止該線程。
Boost.Thread定義包括上述 sleep()函數等十個中斷。 有了這些中斷點,線程可以很容易及時中斷。然而,他們並不總是最佳的選擇,因為中斷點必須事前讀入以檢查 boost::thread_interrupted 異常。
為了提供一個對 Boost.Thread 裡提供的多種函數的整體概述,下面的例子將會再介紹兩個。
#include <boost/thread.hpp> #include <iostream> int main() { std::cout << boost::this_thread::get_id() << std::endl; std::cout << boost::thread::hardware_concurrency() << std::endl; }
使用 boost::this_thread命名空間,能提供獨立的函數應用於當前線程,比如前面出現的sleep() 。另一個是 get_id():它會返回一個當前線程的ID號。它也是由 boost::thread 提供的。
boost::thread 類提供了一個靜態方法 hardware_concurrency() ,它能夠返回基於CPU數目或者CPU核心數目的刻在同時在物理機器上啟動並執行線程數。在常用的雙核機器上調用這個方法,傳回值為2。 這樣的話就可以確定在一個多核程式可以同時啟動並執行理論最大線程數。
三、同步
雖然多線程的使用可以提高應用程式的效能,但也增加了複雜性。如果使用線程在同一時間執行幾個函數,訪問共用資源時必須相應地同步。一旦應用達到了一定規模,這涉及相當一些工作。本段介紹了Boost.Thread提供同步線程的類。
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); mutex.lock(); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; mutex.unlock(); } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
多線程程式使用所謂的互斥對象來同步。Boost.Thread提供多個的互斥類,boost::mutex是最簡單的一個,它的使用就像linux下的二進位互斥量。互斥的基本原則是當一個特定的線程擁有資源的時候防止其他線程奪取其所有權,一旦釋放,其他的線程可以取得所有權。這將導致線程等待至另一個線程完成處理一些操作,從而相應地釋放互斥對象的所有權。
上面的樣本使用一個類型為 boost::mutex 的mutex全域互斥對象。thread()函數擷取此對象的所有權才在 for 迴圈內使用 lock()方法寫入到標準輸出資料流的。一旦資訊被寫入,使用unlock()方法釋放所有權。
main() 建立兩個線程,同時執行thread ()函數。利用 for 迴圈,每個線程數到5,用一個迭代器寫一條訊息到標準輸出資料流。然而,標準輸出資料流是一個全域性的被所有線程共用的對象,該標準不提供任何保證 std::cout 可以安全地從多個線程訪問。 因此,訪問標準輸出資料流必須同步:在任何時候,只有一個線程可以訪問 std::cout。
由於兩個線程試圖在寫入標準輸出資料流前獲得互斥體,實際上只能保證一次只有一個線程訪問 std::cout。不管哪個線程成功調用 lock() 方法,其他所有線程必須等待,直到 unlock() 被調用。
擷取和釋放互斥體是一個典型的模式,是由Boost.Thread通過不同的資料類型支援。 例如,不直接地調用 lock() 和 unlock(),使用 boost::lock_guard 類也是可以的。
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); boost::lock_guard<boost::mutex> lock(mutex); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
boost::lock_guard 在其內部構造和解構函式分別自動調用lock() 和 unlock() 。 訪問共用資源是需要同步的,因為它顯示地被兩個方法調用。 boost::lock_guard 類是另一個出現在我之前第2個系列智能指標單元的RAII用語。
除了boost::mutex 和 boost::lock_guard 之外,Boost.Thread也提供其他的類支援各種同步。其中一個重要的就是 boost::unique_lock ,相比較 boost::lock_guard 而言,它提供許多有用的方法。
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::timed_mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); boost::unique_lock<boost::timed_mutex> lock(mutex, boost::try_to_lock); if (!lock.owns_lock()) lock.timed_lock(boost::get_system_time() + boost::posix_time::seconds(1)); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; boost::timed_mutex *m = lock.release(); m->unlock(); } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
上面的例子用不同的方法來示範 boost::unique_lock 的功能。 當然了,這些功能的用法對給定的情景不一定適用;boost::lock_guard 在上個例子的用法還是挺合理的。 這個例子就是為了示範 boost::unique_lock 提供的功能。
boost::unique_lock 通過多個建構函式來提供不同的方式獲得互斥體。這個期望獲得互斥體的函數簡單地調用了lock()方法,一直等到獲得這個互斥體。所以它的行為跟 boost::lock_guard 的那個是一樣的。
如果第二個參數傳入一個 boost::try_to_lock 類型的值,對應的建構函式就會調用 try_lock方法。這個方法返回 bool 型的值:如果能夠獲得互斥體則返回true,否則返回 false。相比lock函數,try_lock會立即返回,而且在獲得互斥體之前不會被阻塞。
上面的程式向boost::unique_lock 的建構函式的第二個參數傳入boost::try_to_lock。然後通過 owns_lock() 可以檢查是否可獲得互斥體。如果不能, owns_lock() 返回false。這也用到 boost::unique_lock 提供的另外一個函數: timed_lock() 等待一定的時間以獲得互斥體。 給定的程式等待長達1秒,應較足夠的時間來擷取更多的互斥。
其實這個例子顯示了三個方法擷取一個互斥體:lock() 會一直等待,直到獲得一個互斥體。try_lock()則不會等待,但如果它只會在互斥體可用的時候才能獲得,否則返回 false。最後,timed_lock()試圖獲得在一定的時間內擷取互斥體。和try_lock()一樣,返回bool 類型的值意味著成功是否。