在多線程編程中,多個線程協同工作能顯著提升程序效率,但當它們需要共享和操作同一資源時,潛在的問題也隨之而來;線程間的執行順序不確定性可能導致資源競爭,可能引發死鎖,讓程序陷入停滯。
多線程競爭問題示例
我們現在已經知道如何在c++11中創建線程,那么如果多個線程需要操作同一個變量呢?
#include <iostream>
#include <thread>
using namespace std;
int n = 0;
void count10000() {for (int i = 1; i <= 10000; i++)n++;
}
int main() {thread th[100];for (thread &x : th)x = thread(count10000);for (thread &x : th)x.join();cout << n << endl;return 0;
}
可能的兩次輸出分別是:
991164
996417
我們的輸出結果應該是1000000,可是為什么實際輸出結果比1000000小呢?
在多線程的執行順序——同時進行、無次序,所以這樣就會導致一個問題:多個線程進行時,如果它們同時操作同一個變量,那么肯定會出錯。為了應對這種情況,c++11中出現了std::atomic和std::mutex。
std::mutex
std::mutex是 C++11 中最基本的互斥量,一個線程將mutex鎖住時,其它的線程就不能操作mutex,直到這個線程將mutex解鎖。根據這個特性,我們可以修改一下上一個例子中的代碼:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;int n=0;
mutex mtx;
void count10000(){for(auto i=0;i<10000;i++){mtx.lock();n++;mtx.unlock();}
}
int main(){thread th[100];for (thread &x : th)x = thread(count10000);for (thread &x : th)x.join();cout<<n<<endl;return 0;
}
mutex的常用成員函數
std::lock_gard
使用mutex需要上鎖解鎖,但有時由于程序員忘記或者其他奇怪問題時,lock_gard可以自動解鎖。其原理大概是構造時自動上鎖,析構時自動解鎖。示例如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;int n=0;
mutex mtx;
void count100000(){for(auto i=0;i<100000;i++){lock_guard<mutex> lock1(mtx);n++;n--;}
}
int main(){thread th[10];for(int i=0;i<10;i++){th[i]=thread(count100000);}for(int i=0;i<10;i++){th[i].join();}cout<<n<<endl;return 0;
}
std::unique_lock
std::unique_lock是 C++ 標準庫中提供的一個互斥量封裝類,用于在多線程程序中對互斥量進行加鎖和解鎖操作。它的主要特點是可以對互斥量進行更加靈活的管理,包括延遲加鎖、條件變量、超時等。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;int n = 0;
mutex mtx;void count100000() {for (auto i = 0; i < 100; ++i) {unique_lock<mutex> lock1(mtx);n++;lock1.unlock(); // 提前解鎖,體現unique_lock的靈活性this_thread::sleep_for(chrono::milliseconds(1)); // 模擬耗時操作lock1.lock();n--;}
}int main() {thread th[10];for (int i = 0; i < 10; ++i) {th[i] = thread(count100000);}for (int i = 0; i < 10; ++i) {th[i].join(); // 等待所有線程完成}cout << n << endl; // 所有線程結束后輸出結果(理論上應為0)return 0;
}
公共構造函數
函數 | 作用 |
---|---|
unique_lock() noexcept = default | 默認構造函數,創建一個未關聯任何互斥量的std::unique_lock對象。 |
explicit unique_lock(mutex_type& m) | 構造函數,使用給定的互斥量m進行初始化,并對該互斥量進行加鎖操作。 |
unique_lock(mutex_type& m, defer_lock_t) noexcept | 構造函數,使用給定的互斥量m進行初始化,但不對該互斥量進行加鎖操作。 |
unique_lock(mutex_type& m, try_to_lock_t) noexcept | 構造函數,使用給定的互斥量m進行初始化,并嘗試對該互斥量進行加鎖操作。如果加鎖失敗,則創建的std::unique_lock對象不與任何互斥量關聯。 |
unique_lock(mutex_type& m, adopt_lock_t) noexcept | 構造函數,使用給定的互斥量m進行初始化,并假設該互斥量已經被當前線程成功加鎖。 |
std::unique_lock | 使用非常靈活方便,上述操作的使用方式將在課程視頻中作詳細介紹。 |
常用成員函數
函數 | 作用 |
---|---|
lock() | 嘗試對互斥量進行加鎖操作,如果當前互斥量已經被其他線程持有,則當前線程會被阻塞,直到互斥量被成功加鎖。 |
try_lock() | 嘗試對互斥量進行加鎖操作,如果當前互斥量已經被其他線程持有,則函數立即返回false,否則返回true。 |
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time) | 嘗試對互斥量進行加鎖操作,如果當前互斥量已經被其他線程持有,則當前線程會被阻塞,直到互斥量被成功加鎖,或者超過了指定的時間。 |
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time) | 嘗試對互斥量進行加鎖操作,如果當前互斥量已經被其他線程持有,則當前線程會被阻塞,直到互斥量被成功加鎖,或者超過了指定的時間點。 |
unlock() | 對互斥量進行解鎖操作。 |
std::atomic
mutex很好地解決了多線程資源爭搶的問題,但它也有缺點:太……慢……了……
比如前面我們定義了100個thread,每個thread要循環10000次,每次循環都要加鎖、解鎖,這樣固然會浪費很多的時間,那么該怎么辦呢?接下來就是atomic大展拳腳的時間了。
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;atomic<int> n{0};// 列表初始化void count10000(){for(int i=0;i<10000;i++)n++;
}int main(){thread th[10];for(thread& x:th)x=thread(count10000);for(auto& x:th)x.join();cout<<n<<endl;return 0;
}
可以看到,我們只是改動了n的類型(int->std::atomic_int),其他的地方一點沒動,輸出卻正常了。
atomic,本意為原子,可解釋為:
原子操作是最小的且不可并行化的操作。
atomic常用成員函數
死鎖問題
在多個線程中由于上鎖順序問題可能導致線程卡死,如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;int n=0;
mutex mtx1;
mutex mtx2;
void count100000(){for(auto i=0;i<100000;i++){lock_guard<mutex> lock1(mtx1);lock_guard<mutex> lock2(mtx2);n++;n--;}
}
void count200000(){for(auto i=0;i<200000;i++){lock_guard<mutex> lock2(mtx2);lock_guard<mutex> lock1(mtx1);n++;n--;}
}
int main(){thread th[10];for(int i=0;i<10;i++){if(i%2==0)th[i]=thread(count100000);elseth[i]=thread(count200000);}for(int i=0;i<10;i++){th[i].join();}cout<<n<<endl;return 0;
}
這是因為在一個線程count100000中mtx1上鎖后,另一個線程count200000也正好將mtx2上鎖,于是這兩個線程沒辦法獲得另一個mutex,這就是死鎖問題。
解決方法就是保持一樣的上鎖順序,于是當一個線程A搶到第一個mutex時,其他線程無法再獲得mutex,即只能線程A按著順序處理完所有事物。示例如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;int n=0;
mutex mtx1;
mutex mtx2;
void count100000(){for(auto i=0;i<100000;i++){lock_guard<mutex> lock1(mtx1);lock_guard<mutex> lock2(mtx2);n++;n--;}
}
void count200000(){for(auto i=0;i<200000;i++){lock_guard<mutex> lock1(mtx1);lock_guard<mutex> lock2(mtx2);n++;n--;}
}
int main(){thread th[10];for(int i=0;i<10;i++){if(i%2==0)th[i]=thread(count100000);elseth[i]=thread(count200000);}for(int i=0;i<10;i++){th[i].join();}cout<<n<<endl;return 0;
}