編程

Boost Thread 程序設計指南

創建執行緒

就像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>
#include
void hello()
{
std::cout <<
"Hello world, I'm a thread!"
<< std::endl;
}int main( int argc, char * argv[])
{
boost:: thread thrd(&hello);
thrd.join();
return 0;
}

Mutex (mutual exclusion)

任何寫過多執行緒程式的人都知道避免不同執行緒同時訪問共用區域的重要性。如果一個執行緒要改變共用區域中某個資料,而與此同時另一執行緒正在讀這個資料,那麼結果將是未定義的。為了避免這種情況的發生就要使用一些特殊的原始類型和操作。其中最基本的就是互斥體(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中的代碼,這樣就不必手工寫這些函數物件了。

例2:
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include
boost::mutex io_mutex;
struct count
{
count( int id) : id(id) { }
void operator()()
{
for ( int i = 0; i < 10; ++i)
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << id << ": "
<< i << std::endl;
}
}
int id;
};
int main( int argc, char * argv[])
{
boost:: thread thrd1(count(1));
boost:: thread thrd2(count(2));
thrd1.join();
thrd2.join();
return 0;
}

例3: 這個例子和例2一樣,除了使用Boost.Bind來簡化創建執行緒攜帶資料,避免使用函數物件

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include
boost::mutex io_mutex;
void count( int id)
{
for ( int i = 0; i < 10; ++i)
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << id << ": " <<
i << std::endl;
}
}
int main( int argc, char * argv[])
{
boost:: thread thrd1(
boost::bind(&count, 1));
boost:: thread thrd2(
boost::bind(&count, 2));
thrd1.join();
thrd2.join();
return 0;
}

條件變數

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

條件變數的使用總是和互斥體及共用資源聯繫在一起的。執行緒首先鎖住互斥體,然後檢驗共用資源的狀態是否處於可使用的狀態。如果不是,那麼執行緒就要等待條件變數。要指向這樣的操作就必須在等待的時候將互斥體解鎖,以便其他執行緒可以訪問共用資源並改變其狀態。它還得保證從等到得執行緒返回時互斥體是被上鎖得。當另一個執行緒改變了共用資源的狀態時,它就要通知正在等待條件變數得執行緒,並將之返回等待的執行緒。

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

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include
const int BUF_SIZE = 10;
const int ITERS = 100;
boost::mutex io_mutex;
class buffer
{
public :
typedef boost::mutex::scoped_lock
scoped_lock;
buffer()
: p(0), c(0), full(0)
{
}
void put( int m)
{
scoped_lock lock(mutex);
if (full == BUF_SIZE)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout <<
"Buffer is full. Waiting..."
<< std::endl;
}
while (full == BUF_SIZE)
cond.wait(lock);
}
buf[p] = m;
p = (p+1) % BUF_SIZE;
++full;
cond.notify_one();
}
int get()
{
scoped_lock lk(mutex);
if (full == 0)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout <<
"Buffer is empty. Waiting..."
<< std::endl;
}
while (full == 0)
cond.wait(lk);
}

int i = buf

[c][/c]

;
c = (c+1) % BUF_SIZE;
--full;
cond.notify_one();
return i;
}
private :
boost::mutex mutex;
boost::condition cond;
unsigned int p, c, full;
int buf[BUF_SIZE];
};
buffer buf;
void writer()
{
for ( int n = 0; n < ITERS; ++n)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << "sending: "
<< n << std::endl;
}
buf.put(n);
}
}
void reader()
{
for ( int x = 0; x < ITERS; ++x)
{
int n = buf.get();
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << "received: "
<< n << std::endl;
}
}
}
int main( int argc, char * argv[])
{
boost:: thread thrd1(&reader);
boost:: thread thrd2(&writer);
thrd1.join();
thrd2.join();
return 0;
}

執行緒局部存儲

大多數函數都不是可重入的。這也就是說在某一個執行緒已經調用了一個函數時,如果你再調用同一個函數,那麼這樣是不安全的。一個不可重入的函數通過連續的調用來保存靜態變數或者是返回一個指向靜態資料的指標。舉例來說,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。

例5:

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/tss.hpp>
#include <iostream>
boost::mutex io_mutex;
boost::thread_specific_ptr< int > ptr;
struct count
{
           count( int id) : id(id) { }
        void operator()()
           {
                if (ptr.get() == 0)
                   ptr.reset( new int (0));
                for ( int i = 0; i < 10; ++i)
                   {
                           (*ptr)++;
                           boost::mutex::scoped_lock
                           lock(io_mutex);
                           std::cout << id << ": "
                           << *ptr << std::endl;
                   }
           }
        int id;
};
int main( int argc, char * argv[])
{
           boost:: thread thrd1(count(1));
           boost:: thread thrd2(count(2));
           thrd1.join();
           thrd2.join();
        return 0;
}

僅運行一次的常式

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

解決這個問題的方法就是所謂的「一次實現」(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。

#include <boost/thread/thread.hpp>
#include <boost/thread/once.hpp>
#include <iostream>
int i = 0;
boost::once_flag flag =
BOOST_ONCE_INIT;
void init()
{
           ++i;
}
void thread ()
{
           boost::call_once(&init, flag);
}
int main( int argc, char * argv[])
{
           boost:: thread thrd1(& thread );
           boost:: thread thrd2(& thread );
           thrd1.join();
           thrd2.join();
           std::cout << i << std::endl;
        return 0;
}

Boost執行緒庫的未來

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

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

原文地址:http://www.cnblogs.com/hyb1/archive/2013/04/25/3041941.html

 

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0

You may also like

Leave a reply

您的電子郵箱地址不會被公開。 必填項已用 * 標註

此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

More in:編程