@TOC
在C++11標準出台前,多線程編程依賴系統原生接口——Linux的POSIX線程(pthread)與Windows的CreateThread接口互不兼容,導致跨平台代碼開發難度大、可移植性低。C++11首次將多線程納入標準庫,無需依賴第三方庫即可實現跨平台併發編程,同時引入原子操作類,為併發安全提供了原生支持。
一、C++11線程庫核心接口解析
std::thread是線程庫的核心類,搭配系列工具函數可實現線程的創建、管理與同步,常用接口如下:
- 構造函數:
std::thread()創建空線程對象(無關聯執行邏輯);std::thread(fn, args...)創建線程並綁定執行函數fn,args為函數參數。 get_id():返回線程唯一標識ID,用於區分不同線程。join():阻塞主線程,直至目標線程執行完畢,主線程才繼續推進,確保子線程完成任務後再收尾。detach():調用後線程對象與實際線程分離,分離後的線程轉為後台線程,生命週期由系統管理,主線程退出時後台線程可能被強制終止。
跨平台兼容性可通過條件編譯實現,示例如下:
#ifdef _WIN32
// Windows平台原生線程創建
CreateThread(nullptr, 0, ThreadProc, nullptr, 0, nullptr);
#else
// Linux平台POSIX線程創建
pthread_t tid;
pthread_create(&tid, nullptr, ThreadProc, nullptr);
#endif
二、線程庫基礎使用場景
線程執行邏輯的傳入方式主要有三種,分別適用於不同開發場景,以下結合實例詳細説明。
2.1 傳入函數指針
適合複用已定義的函數作為線程執行邏輯,代碼結構清晰,示例如下:
#include <iostream>
#include <thread>
using namespace std;
// 線程執行函數:循環指定次數並打印
void PrintLoop(int loopCount, int threadId) {
for (int i = 0; i < loopCount; i++) {
cout << "線程" << threadId << ":循環輸出第" << i << "次" << endl;
}
cout << endl;
}
int main() {
// 創建兩個線程,分別循環10次和20次
thread t1(PrintLoop, 10, 1);
thread t2(PrintLoop, 20, 2);
// 等待線程執行完畢,避免主線程提前退出
t1.join();
t2.join();
return 0;
}
注:多線程併發執行時,cout輸出可能出現交錯,這是線程調度的正常現象,後續可通過鎖機制優化。
2.2 傳入lambda表達式
無需單獨定義函數,直接嵌入執行邏輯,代碼更簡潔緊湊,示例如下:
#include <iostream>
#include <thread>
using namespace std;
int main() {
int loopA, loopB;
cout << "請輸入兩個線程的循環次數:";
cin >> loopA >> loopB;
// 線程1:使用lambda表達式作為執行邏輯
thread t1([loopA](int threadId) {
for (int i = 0; i < loopA; i++) {
cout << "線程" << threadId << ":循環輸出第" << i << "次" << endl;
}
cout << endl;
}, 1);
// 線程2:複用loopA參數,執行相同邏輯
thread t2([loopA](int threadId) {
for (int i = 0; i < loopA; i++) {
cout << "線程" << threadId << ":循環輸出第" << i << "次" << endl;
}
cout << endl;
}, 2);
t1.join();
t2.join();
return 0;
}
2.3 多線程綜合管理(容器存儲線程)
當需要創建多個線程時,可利用vector容器統一存儲和管理線程對象,避免重複代碼,示例如下:
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
using namespace std;
int main() {
int threadNum;
cout << "請輸入要創建的線程數量:";
cin >> threadNum;
vector<thread> threadList; // 容器存儲線程對象
const size_t loopTimes = 10; // 每個線程的循環次數
// 批量創建線程
for (size_t i = 0; i < threadNum; i++) {
threadList.emplace_back([i, loopTimes]() {
for (int j = 0; j < loopTimes; j++) {
// 打印線程ID和循環次數
cout << "線程" << i + 1 << "(ID:" << this_thread::get_id() << "):第" << j << "次輸出" << endl;
this_thread::sleep_for(chrono::seconds(1)); // 每次輸出後休眠1秒
}
cout << endl;
});
}
// 等待所有線程執行完畢(線程對象不可拷貝,需用引用遍歷)
for (auto& t : threadList) {
t.join();
}
return 0;
}
2.4 線程函數參數傳遞細節
線程函數參數默認採用值拷貝方式傳入,即便參數聲明為引用,也無法修改外部實參(實際引用的是線程棧中的拷貝)。若需修改外部變量,需通過std::ref(引用傳遞)或指針傳遞,示例如下:
#include <iostream>
#include <thread>
using namespace std;
// 引用參數:嘗試修改外部變量
void AddTenRef(int& x) { x += 10; }
// 指針參數:修改外部變量
void AddTenPtr(int* x) { *x += 10; }
int main() {
int baseVal = 10;
// 1. 直接傳遞值:外部變量無變化
thread t1(AddTenRef, baseVal);
t1.join();
cout << "直接傳值後:" << baseVal << endl; // 輸出10(未修改)
// 2. std::ref傳遞引用:外部變量被修改
thread t2(AddTenRef, ref(baseVal));
t2.join();
cout << "std::ref傳引用後:" << baseVal << endl; // 輸出20(已修改)
// 3. 傳遞指針:外部變量被修改
thread t3(AddTenPtr, &baseVal);
t3.join();
cout << "傳遞指針後:" << baseVal << endl; // 輸出30(已修改)
return 0;
}
三、線程安全問題與解決方案
多線程併發訪問共享資源(臨界資源)且存在修改操作時,容易出現數據不一致問題,核心原因是非原子操作的指令交錯執行。
3.1 線程安全問題的本質
以全局變量累加為例,多個線程同時執行total++時,結果往往不符合預期:
#include <iostream>
#include <thread>
using namespace std;
unsigned long total = 0;
// 累加函數:循環num次,每次total自增1
void Accumulate(size_t num) {
for (size_t i = 0; i < num; ++i) {
total++;
}
}
int main() {
cout << "累加前:total = " << total << endl;
// 兩個線程各累加10000000次
thread t1(Accumulate, 10000000);
thread t2(Accumulate, 10000000);
t1.join();
t2.join();
cout << "累加後:total = " << total << endl; // 結果小於20000000
return 0;
}
關鍵原因:total++編譯後會拆分為“讀取total值→修改值→寫入total”三條彙編指令,多線程併發時指令交錯,導致部分修改被覆蓋。
3.2 鎖機制:臨界區原子執行保障
鎖通過限制同一時間只有一個線程進入臨界區,避免指令交錯。C++11提供兩種核心鎖類型:mutex(互斥鎖)和recursive_mutex(遞歸鎖)。
3.2.1 互斥鎖(mutex)
基礎鎖類型,支持lock()(上鎖)和unlock()(解鎖),確保臨界區代碼原子執行,示例如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
using namespace std;
vector<int> dataList;
int count = 0;
mutex mtx; // 互斥鎖
// 向容器添加元素並計數
void AddElement(int n) {
for (int i = 0; i < n; i++) {
mtx.lock(); // 上鎖,進入臨界區
++count;
dataList.push_back(count);
mtx.unlock(); // 解鎖,退出臨界區
cout << "循環進度:" << i + 1 << "/" << n << endl;
}
}
int main() {
size_t start = clock();
// 兩個線程各執行20000次添加操作
thread t1(AddElement, 20000);
thread t2(AddElement, 20000);
t1.join();
t2.join();
size_t end = clock();
cout << "執行耗時:" << (end - start) << "ms" << endl;
cout << "最終計數:" << count << endl; // 輸出40000(正確)
return 0;
}
3.2.2 遞歸鎖(recursive_mutex)
適用於遞歸函數場景,允許同一線程多次上鎖(解鎖次數需與上鎖次數匹配),避免普通互斥鎖導致的死鎖,示例如下:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
recursive_mutex recMtx;
int count = 0;
// 遞歸累加函數
void RecursiveCount(int n) {
if (n == 0) return;
recMtx.lock(); // 遞歸調用時再次上鎖
++count;
RecursiveCount(n - 1);
recMtx.unlock(); // 解鎖次數與上鎖次數一致
}
int main() {
thread t1(RecursiveCount, 1000);
thread t2(RecursiveCount, 1000);
t1.join();
t2.join();
cout << "遞歸累加結果:" << count << endl; // 輸出2000(正確)
return 0;
}
3.3 原子操作:無鎖併發方案
鎖機制會導致線程阻塞,線程數量較多時會影響效率。C++11引入atomic模板,基於CAS(比較並交換)原語實現無鎖原子操作,無需阻塞線程即可保證數據安全。
3.3.1 原子變量直接使用
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic_long atomicTotal{0}; // 原子變量
void AtomicCount(size_t num) {
for (size_t i = 0; i < num; ++i) {
atomicTotal++; // 原子操作,無需加鎖
}
}
int main() {
cout << "累加前:atomicTotal = " << atomicTotal << endl;
thread t1(AtomicCount, 1000000);
thread t2(AtomicCount, 1000000);
t1.join();
t2.join();
cout << "累加後:atomicTotal = " << atomicTotal << endl; // 輸出2000000(正確)
return 0;
}
3.3.2 atomic模板核心特性
atomic<T>可將任意可拷貝類型T封裝為原子類型,核心規則:
- 禁止拷貝構造、移動構造和賦值運算符重載,避免多線程下資源競爭。
- 需通過
load()(讀取)和store()(寫入)操作訪問值(基礎運算符已重載,如++、+=)。
示例代碼:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
void ShowAtomicVal(atomic<int>& val) {
cout << "當前原子值:" << val.load() << endl; // 讀取原子變量
}
int main() {
atomic<int> counter{0}; // 三種等價初始化:counter(0)、counter={0}、counter{0}
const int loopCount = 200;
// 兩個線程各累加200次
thread t1([&]() {
for (int i = 0; i < loopCount; i++) ++counter;
});
thread t2([&]() {
for (int i = 0; i < loopCount; i++) ++counter;
});
ShowAtomicVal(counter); // 輸出0(線程未執行完畢)
t1.join();
t2.join();
cout << "最終原子值:" << counter << endl; // 輸出400(正確)
// 原子變量禁止拷貝
// atomic<int> counter2 = counter; // 編譯失敗
// counter2 = counter; // 編譯失敗
return 0;
}
四、RAII鎖封裝:lock_guard與unique_lock
手動調用lock()和unlock()可能因異常導致鎖未釋放(如臨界區拋出異常,unlock()無法執行)。C++11通過RAII(資源獲取即初始化)機制封裝鎖,確保鎖自動釋放。
4.1 lock_guard:極簡RAII鎖
lock_guard構造時自動上鎖,析構時自動解鎖,適用於無需手動控制鎖狀態的場景,示例如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <stdexcept>
using namespace std;
mutex mtx;
int count = 0;
void SafeAdd(int n) {
for (int i = 0; i < n; i++) {
try {
// 構造時上鎖,出作用域(異常或循環結束)自動解鎖
lock_guard<mutex> lock(mtx);
++count;
// 模擬隨機異常
if (rand() % 4 == 0) {
throw runtime_error("臨界區異常");
}
} catch (const exception& e) {
cout << "異常信息:" << e.what() << endl;
}
}
}
int main() {
thread t1(SafeAdd, 100);
thread t2(SafeAdd, 100);
t1.join();
t2.join();
cout << "最終計數:" << count << endl; // 輸出200(異常不影響鎖釋放)
return 0;
}
4.2 unique_lock:靈活RAII鎖
lock_guard功能單一,unique_lock支持手動控制鎖狀態,提供更多成員函數,適用場景更廣泛,核心接口:
- 上鎖/解鎖:
lock()(阻塞上鎖)、try_lock()(非阻塞上鎖)、try_lock_for()(超時上鎖)、unlock()。 - 狀態管理:
swap()(交換互斥量)、release()(釋放互斥量所有權)。 - 屬性查詢:
owns_lock()(是否持有鎖)、mutex()(獲取互斥量指針)。
示例代碼:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;
mutex mtx;
void FlexibleLockUse() {
unique_lock<mutex> lock(mtx, defer_lock); // defer_lock:延遲上鎖
cout << "是否持有鎖:" << lock.owns_lock() << endl; // 輸出0(未上鎖)
lock.lock(); // 手動上鎖
cout << "是否持有鎖:" << lock.owns_lock() << endl; // 輸出1(已上鎖)
// 模擬業務邏輯
this_thread::sleep_for(chrono::seconds(1));
lock.unlock(); // 手動解鎖
cout << "是否持有鎖:" << lock.owns_lock() << endl; // 輸出0(已解鎖)
// 非阻塞嘗試上鎖
if (lock.try_lock()) {
cout << "二次上鎖成功" << endl;
lock.unlock();
} else {
cout << "二次上鎖失敗" << endl;
}
}
int main() {
thread t(FlexibleLockUse);
t.join();
return 0;
}
五、實戰案例:雙線程交替打印奇偶數
利用condition_variable(條件變量)配合unique_lock,實現兩個線程交替打印1~100的奇偶數,核心邏輯:
cv.wait():阻塞當前線程並釋放鎖,被喚醒後重新申請鎖。cv.notify_one():喚醒一個等待中的線程。- 通過判斷
currentNum的奇偶性,控制線程執行時機。
示例代碼:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mtx;
condition_variable cv;
int currentNum = 1;
const int maxNum = 100;
// 打印奇數
void PrintOddNum() {
while (true) {
unique_lock<mutex> lock(mtx);
// 僅當currentNum為奇數時執行,否則阻塞
cv.wait(lock, []() { return currentNum % 2 != 0; });
if (currentNum > maxNum) break; // 超出範圍退出
cout << "奇數線程(ID:" << this_thread::get_id() << "):" << currentNum << endl;
currentNum++;
cv.notify_one(); // 喚醒偶數線程
}
}
// 打印偶數
void PrintEvenNum() {
while (true) {
unique_lock<mutex> lock(mtx);
// 僅當currentNum為偶數時執行,否則阻塞
cv.wait(lock, []() { return currentNum % 2 == 0; });
if (currentNum > maxNum) break; // 超出範圍退出
cout << "偶數線程(ID:" << this_thread::get_id() << "):" << currentNum << endl;
currentNum++;
cv.notify_one(); // 喚醒奇數線程
}
}
int main() {
thread t1(PrintOddNum);
thread t2(PrintEvenNum);
t1.join();
t2.join();
return 0;
}