在初始化時保護數據
如果你的數據需要在初始化時被保護,就不能再使用mutex了。因為在初始化結束後,這會引起不必要的同步。C++0x提供了很多方法來在初始化時保護數據。
1)假定你的構造函數是用constexpr關鍵字聲明並且滿足常量初始化的條件。在這種情況下,一個靜態存儲區的對象在靜態初始階段會確保在其他代碼運行之前被初始化。對於std::mutex來說,這是最佳選擇,因為它消除了全局mutex初始化時產生紊亂的可能性。
class my_class
{
int i;
public:
constexpr my_class():i(0){}
my_class(int i_):i(i_){}
void do_stuff();
};
my_class x; // static initialization with constexpr constructor
int foo();
my_class y(42+foo()); // dynamic initialization
void f()
{
y.do_stuff(); // is y initialized?
}
2)在一個塊作用域(block scope)中使用靜態變量。在C++0x中,塊作用域的靜態變量在函數第一次被調用時初始化。如果另一個線程在初始化完成之前試圖調用該函數,它必須等待。
void bar()
{
static my_class z(42+foo()); // initialization is thread-safe
z.do_stuff();
}
3)如果以上情況都不適用(對象可能是動態創建),那麼最好使用std::call_once和std::once_flag。從名字就可以看出,std::call_once用於與一個std::once_flag實例協作,指定的函數將只會執行一次。
my_class* p=0;
std::once_flag p_flag;
void create_instance()
{
p=new my_class(42+foo());
}
void baz()
{
std::call_once(p_flag,create_instance);
p->do_stuff();
}
同std::thread構造函數一樣,std::call_once也可以接受函數對象作為參數,並且接受多個參數。再次強調,默認是傳拷貝。如果要傳引用,請使用std::ref.
等待事件
如果想在線程間共享數據,通常需要一個線程等待另一個線程執行某些操作。我們希望這不要花費CPU時間。如果線程只是等待訪問共享數據,那mutex鎖就足夠了。不過,這樣做有時並達不到想要的結果。
最簡單的方法是讓線程Sleep一段時間,然後去檢查是否可以進行想要的操作。一定要確保你用來保護指示事件已經發生的數據的mutex在線程休眠的時候已經unlock(這話是不是聽著很別扭?呵呵,我也不知道怎麼翻譯會容易讓人理解一些。不過看了下面這段代碼,相信你會明白的):
std::mutex m;
bool data_ready;
void process_data();
void foo()
{
std::unique_lock<std::mutex> lk(m);
while(!data_ready)
{
lk.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
lk.lock();
}
process_data();
}
這是最簡單的方法,但是並不好。有兩個原因。第一,在數據准備好之後,線程在被喚醒後去檢查數據之前平均需要等待5毫秒。這有時會造成明顯的滯後。盡管可以通過減少等待時間來解決,但是這會帶來第二個問題:每隔10毫秒,線程必須醒來,獲得mutex,檢查flag,即使什麼也沒有發生。這將耗費CPU時間和增加對mutex的搶占。因此,這將潛在地減慢了正在等待的線程去執行任務。
如果你發現你的代碼是那麼寫的,那麼請用條件變量來代替。不要讓線程等待一段確定的時間,你可以讓線程休眠直到收到另一個線程通知。這可以有效地讓等待線程的CPU使用率為0。我們可以用條件變量來重寫foo函數:
std::mutex m;
std::condition_variable cond;
bool data_ready;
void process_data();
void foo()
{
std::unique_lock<std::mutex> lk(m);
while(!data_ready)
{
cond.wait(lk);
}
process_data();
}
注意上面的代碼把 lock對象lk作為參數傳給了wait()函數。條件變量在wait()函數的unlock這個mutex,在退出函數的時候再將其lock。這樣可以確保線程在休眠時被保護的數據可以被其他線程修改。設置data_ready標志的代碼可以這樣寫:
void set_data_ready()
{
std::lock_guard<std::mutex> lk(m);
data_ready=true;
cond.notify_one();
}
你仍然需要檢查數據是否已經准備好,因為條件變量有可能被惡意喚醒,此時wait()函數將返回盡管它沒有被另一個線程通知。如果你擔心這種情況,你可以讓標准庫來幫你搞定。你只需要指明你在等待什麼即可。
void foo()
{
std::unique_lock<std::mutex> lk(m);
cond.wait(lk,[]{return data_ready;});
process_data();
}