在這個庫最重要的一個類就是
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。 這樣的話就可以确定在一個多核程式可以同時運作的理論最大線程數。
6.3. 同步
雖然多線程的使用可以提高應用程式的性能,但也增加了複雜性。 如果使用線程在同一時間執行幾個函數,通路共享資源時必須相應地同步。 一旦應用達到了一定規模,這涉及相當一些工作。 本段介紹了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
是最簡單的一個。 互斥的基本原則是當一個特定的線程擁有資源的時候防止其他線程奪取其所有權。 一旦釋放,其他的線程可以取得所有權。 這将導緻線程等待至另一個線程完成處理一些操作,進而相應地釋放互斥對象的所有權。
上面的示例使用一個類型為
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
類型的值意味着成功是否。
雖然
boost::mutex
提供了
lock()
和
try_lock()
兩個方法,但是
boost::timed_mutex
隻支援
timed_lock()
,這就是上面示例那麼使用的原因。 如果不用
timed_lock()
的話,也可以像以前的例子那樣用
boost::mutex
。
就像
boost::lock_guard
一樣,
boost::unique_lock
的析構函數也會相應地釋放互斥量。此外,可以手動地用
unlock()
釋放互斥量。也可以像上面的例子那樣,通過調用
release()
解除
boost::unique_lock
和互斥量之間的關聯。然而在這種情況下,必須顯式地調用
unlock()
方法來釋放互斥量,因為
boost::unique_lock
的析構函數不再做這件事情。
boost::unique_lock
這個所謂的獨占鎖意味着一個互斥量同時隻能被一個線程擷取。 其他線程必須等待,直到互斥體再次被釋放。 除了獨占鎖,還有非獨占鎖。 Boost.Thread裡有個
boost::shared_lock
的類提供了非獨占鎖。 正如下面的例子,這個類必須和
boost::shared_mutex
型的互斥量一起使用。
#include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
void wait(int seconds)
{
boost::this_thread::sleep(boost::posix_time::seconds(seconds));
}
boost::shared_mutex mutex;
std::vector<int> random_numbers;
void fill()
{
std::srand(static_cast<unsigned int>(std::time(0)));
for (int i = 0; i < 3; ++i)
{
boost::unique_lock<boost::shared_mutex> lock(mutex);
random_numbers.push_back(std::rand());
lock.unlock();
wait(1);
}
}
void print()
{
for (int i = 0; i < 3; ++i)
{
wait(1);
boost::shared_lock<boost::shared_mutex> lock(mutex);
std::cout << random_numbers.back() << std::endl;
}
}
int sum = 0;
void count()
{
for (int i = 0; i < 3; ++i)
{
wait(1);
boost::shared_lock<boost::shared_mutex> lock(mutex);
sum += random_numbers.back();
}
}
int main()
{
boost::thread t1(fill);
boost::thread t2(print);
boost::thread t3(count);
t1.join();
t2.join();
t3.join();
std::cout << "Sum: " << sum << std::endl;
}
- 下載下傳源代碼
boost::shared_lock
類型的非獨占鎖可以線上程隻對某個資源讀通路的情況下使用。 一個線程修改的資源需要寫通路,是以需要一個獨占鎖。 這樣做也很明顯:隻需要讀通路的線程不需要知道同一時間其他線程是否通路。 是以非獨占鎖可以共享一個互斥體。
在給定的例子,
print()
和
count()
都可以隻讀通路 random_numbers 。 雖然
print()
函數把 random_numbers 裡的最後一個數寫到标準輸出,
count()
函數把它統計到 sum 變量。 由于沒有函數修改 random_numbers,所有的都可以在同一時間用
boost::shared_lock
類型的非獨占鎖通路它。
在
fill()
函數裡,需要用一個
boost::unique_lock
類型的非獨占鎖,因為它插入了一個新的随機數到 random_numbers。 在
unlock()
顯式地調用
unlock()
來釋放互斥量之後,
fill()
等待了一秒。 相比于之前的那個樣子, 在
for
循環的尾部調用
wait()
以保證容器裡至少存在一個随機數,可以被
print()
或者
count()
通路。 對應地,這兩個函數在
for
循環的開始調用了
wait()
。
考慮到在不同的地方每個單獨地調用
wait()
,一個潛在的問題變得很明顯:函數調用的順序直接受CPU執行每個獨立程序的順序決定。 利用所謂的條件變量,可以同步哪些獨立的線程,使數組的每個元素都被不同的線程立即添加到 random_numbers
#include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
boost::mutex mutex;
boost::condition_variable_any cond;
std::vector<int> random_numbers;
void fill()
{
std::srand(static_cast<unsigned int>(std::time(0)));
for (int i = 0; i < 3; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
random_numbers.push_back(std::rand());
cond.notify_all();
cond.wait(mutex);
}
}
void print()
{
std::size_t next_size = 1;
for (int i = 0; i < 3; ++i)
{
boost::unique_lock<boost::mutex> lock(mutex);
while (random_numbers.size() != next_size)
cond.wait(mutex);
std::cout << random_numbers.back() << std::endl;
++next_size;
cond.notify_all();
}
}
int main()
{
boost::thread t1(fill);
boost::thread t2(print);
t1.join();
t2.join();
}
- 下載下傳源代碼
這個例子的程式删除了
wait()
和
count()
。線程不用在每個循環疊代中等待一秒,而是盡可能快地執行。此外,沒有計算總額;數字完全寫入标準輸出流。
為確定正确地處理随機數,需要一個允許檢查多個線程之間特定條件的條件變量來同步不每個獨立的線程。
正如上面所說,
fill()
函數用在每個疊代産生一個随機數,然後放在 random_numbers 容器中。 為了防止其他線程同時通路這個容器,就要相應得使用一個排它鎖。 不是等待一秒,實際上這個例子卻用了一個條件變量。 調用
notify_all()
會喚醒每個哪些正在分别通過調用
wait()
等待此通知的線程。
通過檢視
print()
函數裡的
for
循環,可以看到相同的條件變量被
wait()
函數調用了。 如果這個線程被
notify_all()
喚醒,它就會試圖這個互斥量,但隻有在
fill()
函數完全釋放之後才能成功。
這裡的竅門就是調用
wait()
會釋放相應的被參數傳入的互斥量。 在調用
notify_all()
後,
fill()
函數會通過
wait()
相應地釋放線程。 然後它會阻止和等待其他的線程調用
notify_all()
,一旦随機數已寫入标準輸出流,這就會在
print()
裡發生。
注意到在
print()
函數裡調用
wait()
事實上發生在一個單獨
while
循環裡。 這樣做的目的是為了處理在
print()
函數裡第一次調用
wait()
函數之前随機數已經放到容器裡。 通過比較 random_numbers
6.4. 線程本地存儲
線程本地存儲(TLS)是一個隻能由一個線程通路的專門的存儲區域。 TLS的變量可以被看作是一個隻對某個特定線程而非整個程式可見的全局變量。 下面的例子顯示了這些變量的好處。
#include <boost/thread.hpp>
#include <iostream>
#include <cstdlib>
#include <ctime>
void init_number_generator()
{
static bool done = false;
if (!done)
{
done = true;
std::srand(static_cast<unsigned int>(std::time(0)));
}
}
boost::mutex mutex;
void random_number_generator()
{
init_number_generator();
int i = std::rand();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << i << std::endl;
}
int main()
{
boost::thread t[3];
for (int i = 0; i < 3; ++i)
t[i] = boost::thread(random_number_generator);
for (int i = 0; i < 3; ++i)
t[i].join();
}
- 下載下傳源代碼
該示例建立三個線程,每個線程寫一個随機數到标準輸出流。
random_number_generator()
函數将會利用在C++标準裡定義的
std::rand()
函數建立一個随機數。 但是用于
std::rand()
的随機數産生器必須先用
std::srand()
正确地初始化。 如果沒做,程式始終列印同一個随機數。
随機數産生器,通過
std::time()
傳回目前時間, 在
init_number_generator()
函數裡完成初始化。 由于這個值每次都不同,可以保證産生器總是用不同的值初始化,進而産生不同的随機數。 因為産生器隻要初始化一次,
init_number_generator()
用了一個靜态變量 done
如果程式運作了多次,寫入的三分之二的随機數顯然就會相同。 事實上這個程式有個缺陷:
std::rand()
所用的産生器必須被各個線程初始化。 是以
init_number_generator()
的實作實際上是不對的,因為它隻調用了一次
std::srand()
。使用TLS,這一缺陷可以得到糾正。
#include <boost/thread.hpp>
#include <iostream>
#include <cstdlib>
#include <ctime>
void init_number_generator()
{
static boost::thread_specific_ptr<bool> tls;
if (!tls.get())
tls.reset(new bool(false));
if (!*tls)
{
*tls = true;
std::srand(static_cast<unsigned int>(std::time(0)));
}
}
boost::mutex mutex;
void random_number_generator()
{
init_number_generator();
int i = std::rand();
boost::lock_guard<boost::mutex> lock(mutex);
std::cout << i << std::endl;
}
int main()
{
boost::thread t[3];
for (int i = 0; i < 3; ++i)
t[i] = boost::thread(random_number_generator);
for (int i = 0; i < 3; ++i)
t[i].join();
}
- 下載下傳源代碼
用一個TLS變量 tls 代替靜态變量 done,是基于用
bool
類型執行個體化的
boost::thread_specific_ptr
。 原則上, tls 工作起來就像 done :它可以作為一個條件指明随機數發生器是否被初始化。 但是關鍵的差別,就是 tls
一旦一個
boost::thread_specific_ptr
型的變量被建立,它可以相應地設定。 不過,它期望得到一個
bool
型變量的位址,而非它本身。使用
reset()
方法,可以把它的位址儲存到 tls 裡面。 在給出的例子中,會動态地配置設定一個
bool
型的變量,由
new
傳回它的位址,并儲存到 tls 裡。 為了避免每次調用
init_number_generator()
都設定 tls ,它會通過
get()
函數檢查是否已經儲存了一個位址。