Boost學習系列6 - 多線程 (上)

作者: juan001 (2 篇文章) 日期: 九月 15, 2011 在 2:50 下午

 

一、概述
線程是在同一程序同一時間內允許執行不同函數的離散處理隊列,這使得在一個長時間進行某種特殊運算的函數在執行時不阻礙其他的函數時變得十分重要。線程實際上允許同時執行兩種函數,而這兩者不必相互等待。
一旦一個應用程序啓動,它僅包含一個默認線程。此線程執行main()函數。在main()中被調用的函數則按這個線程的上下文順序地執行,這樣的程序稱爲單線程程序。
反之,那些創建新的線程的程序就是多線程程序。他們不僅可以在同一時間執行多個函數,而且這在如今多核盛行的時代顯得尤爲重要。既然多核允許同時執行多個函數,這就使得對開發人員相應地使用這種處理能力提出了要求。然而線程一直被用來當併發地執行多個函數,開發人員現在不得不仔細地構建應用來支持這種併發。多線程編程知識也因此在多核系統時代變得越來越重要。
本章介紹的是C++ Boost庫Boost.Thread,它可以開發獨立於平臺的多線程應用程序。
二、線程管理
這個庫中最重要的一個類就是boost::thread,它在boost/thread.hpp裏定義,用來創建一個新線程。下面的示例來說明如何運用它。
view plain
#include
#include

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 用來管理和處理時間的數據。
雖然前面的例子說明了如何等待一個不同的線程,但下面的例子演示瞭如何通過所謂的中斷點讓一個線程中斷。
view plain
#include
#include

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 裏提供的多種函數的整體概述,下面的例子將會再介紹兩個。
view plain
#include
#include

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提供同步線程的類。
view plain
#include
#include

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 類也是可以的。
view plain
#include
#include

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 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 而言,它提供許多有用的方法。
view plain
#include
#include

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 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 <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 類型的值意味着成功是否。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章