使用 sleep(1)
這幾天學習多線程裡的互斥鎖和條件變量,想要實作一個功能,線程 A 對一個全局變量進行遞增操作,當變量符合某個要求的時候,用線程 B 輸出。
代碼内容:線程 1 使用 add 方法對 point 進行遞增到 100,point 可以被 5 整除時線程 B 對其進行輸出。
實作方法1:
// compiler g++ 4.8
mutex mt;
condition_variable cv_1;
static int point = 0;
int status = 0;
void add(){ //線程1
int times = 100;
while(times--){
unique_lock<mutex> lk(mt);
while(status != 0){
cv_1.wait(lk);
}
point++;
if(point%5 == 0){
lk.unlock();
sleep(1); //(1)
}
}
}
void print(){ //線程2
int times = 100 / 5;
while(times--){
unique_lock<mutex> lk(mt);
cout << "point: " << point << endl;
status = 0;
cv_1.notify_one();
lk.unlock();
sleep(1); //(2)
}
}
int main(){
thread t1 = thread(add);
thread t2 = thread(print);
t1.join();
t2.join();
return 0;
}
代碼的 (1) (2) 部分使用了
sleep
函數,這個
sleep
發揮了至關重要的作用。
(1) 的上一條語句解鎖了互斥鎖,此時線程 1 和線程 2 都在競争這個鎖,到底誰會動作快一點拿到鎖呢?我們的目的是線程 2 拿到鎖。但是實踐表明線程 2 比較拉給,搶不過線程 1。不過在加上了
sleep(1)
之後,線程 2 就能成功拿到鎖了,符合我們的要求。
個人思考
這個代碼實作是沒有問題的,我的問題在于使用
sleep(1)
這個函數。線程 1 會比線程 2 快多少?0.5s?0.1s?這些都是不确定的,而直接讓線程休眠 1 秒,經過試驗,OK 可以成功運作了,那就 1 秒。我覺得這是很“模糊”,很“浪費”的一個行為。1 秒可以運作多少代碼了。。。
我看網上有挺多文章寫條件變量的代碼時都是直接用的
sleep
。應該用其他更好的方法實作才對,而不是因為sleep可以使代碼運作符合要求就讓線程每次都強迫休眠 1 秒。可以使用條件變量來控制線程的執行順序。
使用條件變量确定
mutex mt;
condition_variable cv_1, cv_2;
static int point = 0;
int status = 0;
void add(){
int times = 100;
while(times--){
unique_lock<mutex> lk(mt);
while(status != 0){
cv_1.wait(lk);
}
point++;
if(point%5 == 0){
status = 1;
cv_1.notify_one();
}
} //(1)
}
void print(){
int times = 100 / 5;
while(times--){
unique_lock<mutex> lk(mt); //(2)
while(status != 1){
cv_1.wait(lk); //(3)
}
cout << "point: " << point << endl;
status = 0;
cv_1.notify_one();
}
}
int main(){
thread t1 = thread(add);
thread t2 = thread(print);
t1.join();
t2.join();
return 0;
}
代碼分析
使用 wait 方法可以使線程阻塞,釋放互斥鎖,這樣就可以 100% 讓其他線程拿到鎖了。
在上面的代碼中,當point符合要求時,線程 1 進行notify 。等到循環一次結束,運作到 (1) 時,
unique_lock
析構會釋放鎖,但我們仍然不知道哪個線程先拿到鎖。
此時有兩種情況:
(1)如果是線程 1
unique_lock<mutex> lk(mt)
拿到鎖,就會進入
cv_1.wait(lk)
的阻塞狀态,釋放鎖。然後線程 2 就可以拿到鎖運作了。
(2)線程 2 直接拿到鎖運作,和 (1) 一樣的結果。
線程 2 拿到鎖時有兩種情況:
(1)正阻塞在代碼中 (2) 的位置,這時候因為線程 1 已經執行過
cv_1.notify_one()
了,是以線程 2 中的
while(status != 1)
肯定為否,會跳過
cv_1.wait(lk)
直接執行後面的代碼。(符合要求)
(2)正阻塞在代碼中 (3) 的位置,
cv_1.wait(lk)
在被線程 1 notify後就一直請求着鎖,現在拿到了鎖,可以執行
cv_1.wait(lk)
語句後面的代碼了。和 (1) 一樣的結果。
這裡對這種情況加以解釋:
如果線程 1
notify
的時候線程 2 沒有進入
wait
阻塞狀态怎麼辦?不就信号丢失了嗎?
這正是
while
判斷的魅力,如果 1 已經
notify
過了,說明 2 中的
while
判斷裡面的語句為假,2 此時運作就不會進入
wait
的
while
循環了。會直接執行後面的代碼。這和線程 2 處于
wait
阻塞狀态時收到線程 1 的通知是一樣的。(同樣的,也适用于線程 2 通知的時候線程 1 沒有進入
wait
阻塞狀态的情況)。
while 判斷的另一個作用是防止虛假喚醒。總之就是 wait 在被喚醒的時候資料不一定是符合要求的,是以要用 while 循環,直到資料符合條件才跳出。
改進
把 add 和 print 函數中的
unique_lock<mutex> lk(mt)
放到函數體的第一句,這樣在 while 循環過程中
lk
就不會析構,就會一直占有着鎖,直到運作
cv_1.wait(lk)
才會進入阻塞狀态,釋放鎖。
這樣線程的執行流程就很清楚了:線上程 1 滿足條件 notify 線程 2 後,線程 2 第一時間并沒有拿到鎖,而是等到線程 1 阻塞之後才拿到鎖,然後線程 2 執行代碼後,notify 線程 1,線程 1 也是等到線程 2 阻塞之後才拿到鎖,解除阻塞狀态,繼續運作。
void add(){
unique_lock<mutex> lk(mt);
int times = 100;
while(times--){
//unique_lock<mutex> lk(mt); //本來在這
while(status != 0){
cv_1.wait(lk);
}
point++;
if(point%5 == 0){
status = 1;
cv_1.notify_one();
}
} //(1)
}
反思
我在上面代碼分析裡的分幾種情況讨論太過複雜,沒有必要。其實隻要關注哪個線程的條件變量符合要求,可以運作代碼就行了。其他不符合要求的線程,不管是沒有搶到鎖阻塞在
unique_lock<mutex> lk(mt)
,還是搶到了阻塞在
cv_1.wait(lk)
,他們都沒有占有鎖,進而讓那些符合要求的線程可以運作,并且這些的阻塞在符合條件變量的要求後,都會執行相同的邏輯代碼,是一樣的。
要是代碼的邏輯都能像上面改進後的代碼的邏輯這樣簡單粗暴就好了。
總結
對于不确定鎖的争奪的情況,可以用condition_variable.wait(mutex) 方法,使線程阻塞并且釋放鎖,這樣就確定其他線程可以拿到鎖了。要注意的是不同線程之間的“條件變量”的邏輯關系。
在分析類似的多線程使用條件變量确定執行順序的代碼時,不用太過于執着分析鎖的競争情況,把重點放在符合條件不會阻塞的線程上,然後根據條件變量的改變去判斷接下來執行的線程。
有錯請指正,感謝