@TOC


在C++11標準出台前,多線程編程依賴系統原生接口——Linux的POSIX線程(pthread)與Windows的CreateThread接口互不兼容,導致跨平台代碼開發難度大、可移植性低。C++11首次將多線程納入標準庫,無需依賴第三方庫即可實現跨平台併發編程,同時引入原子操作類,為併發安全提供了原生支持。

一、C++11線程庫核心接口解析

std::thread是線程庫的核心類,搭配系列工具函數可實現線程的創建、管理與同步,常用接口如下:

  • 構造函數:std::thread()創建空線程對象(無關聯執行邏輯);std::thread(fn, args...)創建線程並綁定執行函數fnargs為函數參數。
  • 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;
}

C++11線程庫_#include


注:多線程併發執行時,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;
}

C++11線程庫_ios_02

關鍵原因: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;
}