C++學習:六個月從基礎到就業——多線程編程:std::thread基礎
本文是我C++學習之旅系列的第五十四篇技術文章,也是第四階段"并發與高級主題"的第一篇,介紹C++11引入的多線程編程基礎知識。查看完整系列目錄了解更多內容。
引言
在現代計算機科學中,多線程編程已成為提高程序性能的關鍵技術。隨著多核處理器的普及,有效利用并行計算能力變得日益重要。C++11標準引入了線程支持庫,使C++開發者能夠直接在語言層面進行多線程編程,無需依賴操作系統特定的API或第三方庫。
本文將深入介紹C++11的std::thread
類的基礎知識,包括線程的創建、管理、參數傳遞、異常處理以及線程同步的基本概念。通過本文的學習,你將能夠編寫基本的多線程C++程序,為后續深入學習并發編程打下基礎。
目錄
- 多線程編程:std::thread基礎
- 引言
- 目錄
- 多線程編程基礎
- 并發與并行
- 多線程的優勢
- 多線程的挑戰
- std::thread類
- 線程的創建
- 函數對象與Lambda表達式
- 成員函數作為線程函數
- 線程的參數傳遞
- 基本參數傳遞
- 引用參數的傳遞
- 移動語義與線程
- 線程的生命周期管理
- join操作
- detach操作
- 可連接狀態檢查
- 線程標識符與線程本地存儲
- 獲取線程ID
- 線程本地存儲
- 線程與異常處理
- 線程函數中的異常
- RAII與線程管理
- 實際應用案例
- 并行計算示例
- 后臺任務處理
- 用戶界面響應性改進
- 常見問題與注意事項
- 競態條件
- 死鎖與活鎖
- 線程數量的選擇
- 調試多線程程序
- 總結
多線程編程基礎
并發與并行
在討論多線程編程之前,我們需要理解兩個基本概念:并發(Concurrency)和并行(Parallelism)。
并發是指程序的不同部分可以"同時"執行,但實際上可能是通過時間片輪轉在單核處理器上交替執行。并發是一個程序結構概念,強調的是任務的獨立性。
并行是指程序的不同部分真正同時執行,通常在多核處理器上。并行是一個執行概念,強調的是性能的提升。
#include <iostream>
#include <thread>void printMessage(const std::string& message) {std::cout << message << std::endl;
}int main() {// 創建兩個線程,在多核處理器上可能并行執行std::thread t1(printMessage, "Hello from thread 1!");std::thread t2(printMessage, "Hello from thread 2!");// 等待線程完成t1.join();t2.join();return 0;
}
多線程的優勢
多線程編程具有以下主要優勢:
-
提高性能:通過并行處理,多線程可以更有效地利用多核處理器,加速計算密集型任務。
-
響應性增強:在用戶界面應用中,使用獨立線程處理耗時操作可以保持界面響應迅速。
-
資源利用率提高:當一個線程等待I/O操作完成時,其他線程可以繼續執行,提高整體資源利用率。
-
簡化復雜問題:某些問題在多線程模型下更容易表達和理解。
多線程的挑戰
盡管多線程編程帶來諸多優勢,但也面臨以下挑戰:
-
同步問題:多線程訪問共享資源需要適當同步,否則會導致數據競爭和不確定行為。
-
死鎖風險:不當的線程同步可能導致死鎖,使程序永久卡住。
-
調試困難:多線程程序的執行具有不確定性,使得調試更加復雜。
-
可伸縮性問題:創建過多線程會導致線程切換開銷增加,反而降低性能。
-
設計復雜性:多線程程序的設計和實現通常比單線程程序更復雜。
std::thread類
C++11引入的std::thread
類是C++標準庫中進行多線程編程的核心組件。它封裝了操作系統的線程API,提供了平臺無關的線程管理功能。
線程的創建
創建線程的最基本方式是構造一個std::thread
對象,并傳遞一個可調用對象(函數、函數對象或lambda表達式)作為線程函數:
#include <iostream>
#include <thread>// 普通函數作為線程函數
void hello() {std::cout << "Hello from thread!" << std::endl;
}int main() {// 創建線程,執行hello函數std::thread t(hello);// 等待線程完成t.join();std::cout << "Main thread continues execution." << std::endl;return 0;
}
線程創建后會立即開始執行,與主線程并發運行。在上面的例子中,主線程通過調用join()
方法等待新線程完成。
函數對象與Lambda表達式
除了普通函數外,我們還可以使用函數對象和Lambda表達式作為線程函數:
#include <iostream>
#include <thread>// 函數對象
class Task {
public:void operator()() const {std::cout << "Task is executing in thread." << std::endl;}
};int main() {// 使用函數對象Task task;std::thread t1(task);t1.join();// 使用臨時函數對象(需要額外的括號避免語法解析歧義)std::thread t2((Task())); // 額外的括號t2.join();// 使用Lambda表達式std::thread t3([]() {std::cout << "Lambda is executing in thread." << std::endl;});t3.join();return 0;
}
注意,當使用臨時函數對象時,需要額外的括號避免"最令人恐懼的解析"(most vexing parse)問題,否則編譯器會將std::thread t2(Task());
解釋為一個函數聲明,而不是對象定義。
成員函數作為線程函數
線程函數也可以是類的成員函數,但需要提供一個對象實例:
#include <iostream>
#include <thread>class Counter {
private:int count = 0;public:void increment(int times) {for (int i = 0; i < times; ++i) {++count;}std::cout << "Final count: " << count << std::endl;}
};int main() {Counter counter;// 創建線程執行成員函數std::thread t(&Counter::increment, &counter, 1000000);t.join();return 0;
}
在上面的例子中,我們傳遞了成員函數指針、對象指針和函數參數給std::thread
構造函數。
線程的參數傳遞
基本參數傳遞
向線程函數傳遞參數非常簡單,只需在std::thread
構造函數中的線程函數參數后添加額外的參數:
#include <iostream>
#include <thread>
#include <string>void printMessage(const std::string& message, int count) {for (int i = 0; i < count; ++i) {std::cout << message << " " << i << std::endl;}
}int main() {// 傳遞兩個參數給線程函數std::thread t(printMessage, "Message", 5);t.join();return 0;
}
需要注意的是,參數是以值傳遞的方式傳給線程函數的,即使函數參數聲明為引用類型。
引用參數的傳遞
如果要傳遞引用,需要使用std::ref
或std::cref
包裝器:
#include <iostream>
#include <thread>
#include <string>
#include <functional> // 為std::ref和std::crefvoid modifyString(std::string& str) {str += " - Modified by thread";
}int main() {std::string message = "Original message";// 使用std::ref傳遞引用std::thread t(modifyString, std::ref(message));t.join();std::cout << "After thread: " << message << std::endl;return 0;
}
不使用std::ref
的話,線程函數會收到message的一個副本,而不是引用,修改不會影響原始變量。
移動語義與線程
C++11的移動語義在線程參數傳遞中非常有用,尤其是對于不可復制但可移動的對象:
#include <iostream>
#include <thread>
#include <memory>
#include <vector>void processUniquePtr(std::unique_ptr<int> ptr) {// 處理獨占指針*ptr += 10;std::cout << "Value in thread: " << *ptr << std::endl;
}int main() {// 創建一個獨占指針auto ptr = std::make_unique<int>(42);// 使用std::move轉移所有權到線程std::thread t(processUniquePtr, std::move(ptr));// 此時ptr為nullptrif (ptr == nullptr) {std::cout << "Original pointer is now nullptr" << std::endl;}t.join();return 0;
}
在上面的例子中,我們使用std::move
將unique_ptr
的所有權轉移到線程函數中。這是必要的,因為unique_ptr
不可復制,只能移動。
線程的生命周期管理
join操作
join()
方法用于等待線程完成。調用線程會阻塞,直到目標線程執行完畢:
#include <iostream>
#include <thread>
#include <chrono>void longTask() {// 模擬耗時任務std::cout << "Long task started" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Long task completed" << std::endl;
}int main() {std::cout << "Main thread starting" << std::endl;std::thread t(longTask);std::cout << "Main thread waiting for worker thread..." << std::endl;t.join(); // 主線程阻塞,等待t完成std::cout << "Worker thread has completed. Main thread continues." << std::endl;return 0;
}
需要注意的是,一個線程只能被join()
一次。嘗試多次join()
同一個線程會導致未定義行為,通常會拋出異常。
detach操作
detach()
方法用于將線程與std::thread
對象分離。分離后,線程會在后臺獨立運行,不再受std::thread
對象的控制:
#include <iostream>
#include <thread>
#include <chrono>void backgroundTask() {std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Background task completed" << std::endl;
}int main() {{std::cout << "Creating a detached thread" << std::endl;std::thread t(backgroundTask);t.detach(); // 線程在后臺運行,不等待它完成std::cout << "Thread detached, main thread continues..." << std::endl;} // t銷毀,但線程繼續在后臺運行// 睡眠足夠長的時間,確保能看到后臺線程的輸出std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "Main thread ending" << std::endl;return 0;
}
使用detach()
時需要特別小心:
- 分離后無法再獲取線程的控制權
- 主線程結束時,即使后臺線程還在運行,程序也會終止
- 要確保線程訪問的資源在線程運行期間保持有效
可連接狀態檢查
線程對象有兩種狀態:可連接(joinable)和不可連接(non-joinable)。只有處于可連接狀態的線程才能被join()
或detach()
:
#include <iostream>
#include <thread>void simpleTask() {std::cout << "Task executing..." << std::endl;
}int main() {// 默認構造的線程對象是不可連接的std::thread t1;std::cout << "t1 joinable: " << t1.joinable() << std::endl; // 輸出:0// 初始化后的線程是可連接的std::thread t2(simpleTask);std::cout << "t2 joinable: " << t2.joinable() << std::endl; // 輸出:1// join后線程變為不可連接t2.join();std::cout << "After join, t2 joinable: " << t2.joinable() << std::endl; // 輸出:0// 創建另一個線程std::thread t3(simpleTask);std::cout << "t3 joinable: " << t3.joinable() << std::endl; // 輸出:1// detach后線程變為不可連接t3.detach();std::cout << "After detach, t3 joinable: " << t3.joinable() << std::endl; // 輸出:0return 0;
}
以下情況下線程是不可連接的:
- 默認構造的
std::thread
對象 - 已經被
join()
或detach()
的線程 - 通過移動操作轉移了所有權的線程
線程標識符與線程本地存儲
獲取線程ID
每個線程都有一個唯一的標識符,可以通過get_id()
方法或std::this_thread::get_id()
獲取:
#include <iostream>
#include <thread>
#include <sstream>// 打印當前線程ID的輔助函數
std::string getThreadIdString() {std::ostringstream oss;oss << std::this_thread::get_id();return oss.str();
}void threadFunction() {std::cout << "Thread function running in thread " << getThreadIdString() << std::endl;
}int main() {std::cout << "Main thread ID: " << getThreadIdString() << std::endl;std::thread t(threadFunction);std::cout << "Created thread with ID: " << t.get_id() << std::endl;t.join();// join后,線程ID變為默認值std::cout << "After join, thread ID: " << t.get_id() << std::endl;return 0;
}
線程ID可用于識別和區分不同的線程,在調試和日志記錄中特別有用。
線程本地存儲
線程本地存儲(Thread Local Storage, TLS)允許每個線程擁有變量的私有副本。C++11引入了thread_local
關鍵字來聲明線程局部變量:
#include <iostream>
#include <thread>
#include <string>// 線程局部變量
thread_local int counter = 0;
thread_local std::string threadName = "Unknown";void incrementCounter(const std::string& name) {threadName = name; // 設置此線程的名稱for (int i = 0; i < 5; ++i) {++counter; // 遞增此線程的計數器std::cout << "Thread " << threadName << ": counter = " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(10));}
}int main() {// 在主線程中訪問threadName = "Main";std::cout << "Initial counter in main thread: " << counter << std::endl;// 創建兩個線程,各自擁有counter的副本std::thread t1(incrementCounter, "Thread1");std::thread t2(incrementCounter, "Thread2");// 在主線程中遞增counterfor (int i = 0; i < 3; ++i) {++counter;std::cout << "Thread " << threadName << ": counter = " << counter << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(10));}t1.join();t2.join();// 主線程中的counter不受其他線程影響std::cout << "Final counter in main thread: " << counter << std::endl;return 0;
}
線程本地存儲的使用場景:
- 線程安全的單例模式
- 每線程緩存
- 線程特定的狀態信息
- 避免使用互斥量的簡單線程隔離
線程與異常處理
線程函數中的異常
線程函數中拋出的異常不會傳播到創建線程的上下文中。如果不在線程內部捕獲異常,程序將調用std::terminate
終止:
#include <iostream>
#include <thread>
#include <stdexcept>void threadWithException() {try {std::cout << "Thread starting..." << std::endl;throw std::runtime_error("Exception in thread!");}catch (const std::exception& e) {std::cout << "Caught exception in thread: " << e.what() << std::endl;}
}void threadWithUncaughtException() {std::cout << "Thread starting..." << std::endl;throw std::runtime_error("Uncaught exception in thread!");// 這個異常不會被捕獲,程序將終止
}int main() {// 正確處理異常的線程std::thread t1(threadWithException);t1.join();std::cout << "After first thread" << std::endl;// 包含未捕獲異常的線程 - 會導致程序終止// std::thread t2(threadWithUncaughtException);// t2.join();std::cout << "Main thread ending" << std::endl;return 0;
}
由于線程異常不會傳播,正確的線程設計應在線程函數內部捕獲和處理所有可能的異常。
RAII與線程管理
在C++中,我們常常使用RAII(Resource Acquisition Is Initialization)模式來確保資源的正確釋放。對于線程管理,這一點也很重要,可以確保線程始終被正確地join()
或detach()
:
#include <iostream>
#include <thread>// 線程包裝器,實現RAII
class ThreadGuard {
private:std::thread& t;public:// 構造函數接收線程引用explicit ThreadGuard(std::thread& t_) : t(t_) {}// 析構函數確保線程被join~ThreadGuard() {if (t.joinable()) {t.join();}}// 禁止復制和賦值ThreadGuard(const ThreadGuard&) = delete;ThreadGuard& operator=(const ThreadGuard&) = delete;
};void someFunction() {std::cout << "Thread function executing..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Thread function completed." << std::endl;
}int main() {try {std::thread t(someFunction);ThreadGuard guard(t); // RAII包裝器確保t被join// 模擬異常// throw std::runtime_error("Simulated exception");std::cout << "Main thread continuing..." << std::endl;}catch (const std::exception& e) {std::cout << "Exception caught: " << e.what() << std::endl;}std::cout << "Main thread exiting safely." << std::endl;return 0;
}
C++17引入了std::jthread
類,它是std::thread
的改進版本,自動實現了RAII模式,并提供了取消線程的能力。在C++20中,它已成為標準的一部分。
實際應用案例
并行計算示例
以下是一個使用多線程并行計算向量點積的例子:
#include <iostream>
#include <vector>
#include <thread>
#include <numeric>
#include <functional>
#include <future>// 計算部分點積
double partialDotProduct(const std::vector<double>& v1, const std::vector<double>& v2,size_t start, size_t end) {return std::inner_product(v1.begin() + start, v1.begin() + end,v2.begin() + start, 0.0);
}// 并行計算點積
double parallelDotProduct(const std::vector<double>& v1,const std::vector<double>& v2,unsigned numThreads) {std::vector<std::future<double>> futures(numThreads);std::vector<std::thread> threads(numThreads);// 計算每個線程處理的元素數量size_t length = v1.size();size_t blockSize = length / numThreads;// 啟動線程for (unsigned i = 0; i < numThreads; ++i) {// 計算當前線程處理的范圍size_t start = i * blockSize;size_t end = (i == numThreads - 1) ? length : (i + 1) * blockSize;// 創建promise和futurestd::promise<double> promise;futures[i] = promise.get_future();// 創建線程threads[i] = std::thread([&v1, &v2, start, end, promise = std::move(promise)]() mutable {double result = partialDotProduct(v1, v2, start, end);promise.set_value(result);});}// 等待所有線程完成并獲取結果double result = 0.0;for (unsigned i = 0; i < numThreads; ++i) {threads[i].join();result += futures[i].get();}return result;
}int main() {// 創建兩個測試向量std::vector<double> v1(1'000'000, 1.0);std::vector<double> v2(1'000'000, 2.0);// 單線程計算auto start = std::chrono::high_resolution_clock::now();double singleThreadResult = std::inner_product(v1.begin(), v1.end(), v2.begin(), 0.0);auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> singleThreadTime = end - start;// 多線程計算start = std::chrono::high_resolution_clock::now();unsigned numThreads = std::thread::hardware_concurrency(); // 獲取CPU核心數double multiThreadResult = parallelDotProduct(v1, v2, numThreads);end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> multiThreadTime = end - start;// 輸出結果std::cout << "Single thread result: " << singleThreadResult << " (Time: " << singleThreadTime.count() << "ms)" << std::endl;std::cout << "Multi thread result: " << multiThreadResult << " (Time: " << multiThreadTime.count() << "ms)" << std::endl;std::cout << "Speedup: " << singleThreadTime.count() / multiThreadTime.count()<< "x" << std::endl;return 0;
}
在這個例子中,我們將大向量分成多個塊,由不同線程計算部分點積,然后匯總結果。在多核處理器上,這種并行計算通常能顯著提高性能。
后臺任務處理
多線程也常用于執行不應阻塞主線程的后臺任務,如下載、IO操作等:
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>// 線程安全的任務隊列
template<typename T>
class TaskQueue {
private:std::queue<T> queue_;std::mutex mutex_;std::condition_variable cond_;std::atomic<bool> quit_{false};public:// 添加任務到隊列void push(T item) {{std::lock_guard<std::mutex> lock(mutex_);queue_.push(std::move(item));}cond_.notify_one(); // 通知一個等待線程}// 從隊列獲取任務bool pop(T& item) {std::unique_lock<std::mutex> lock(mutex_);// 等待直到隊列有元素或收到退出信號cond_.wait(lock, [this] { return !queue_.empty() || quit_; });// 如果是退出信號且隊列為空,返回falseif (queue_.empty()) return false;item = std::move(queue_.front());queue_.pop();return true;}// 設置退出信號void quit() {quit_ = true;cond_.notify_all(); // 通知所有等待線程}// 檢查隊列是否為空bool empty() const {std::lock_guard<std::mutex> lock(mutex_);return queue_.empty();}
};// 模擬文件下載任務
void downloadFile(const std::string& url) {std::cout << "Downloading: " << url << "..." << std::endl;// 模擬下載時間std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Download completed: " << url << std::endl;
}// 后臺下載線程函數
void downloadWorker(TaskQueue<std::string>& taskQueue) {std::string url;// 循環處理隊列中的任務while (taskQueue.pop(url)) {downloadFile(url);}std::cout << "Download worker exiting..." << std::endl;
}int main() {TaskQueue<std::string> downloadQueue;// 創建后臺工作線程std::thread workerThread(downloadWorker, std::ref(downloadQueue));// 添加下載任務downloadQueue.push("http://example.com/file1.zip");downloadQueue.push("http://example.com/file2.zip");downloadQueue.push("http://example.com/file3.zip");// 模擬主線程其他工作for (int i = 0; i < 5; ++i) {std::cout << "Main thread doing other work..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}// 添加更多任務downloadQueue.push("http://example.com/file4.zip");downloadQueue.push("http://example.com/file5.zip");// 等待所有任務完成while (!downloadQueue.empty()) {std::cout << "Waiting for downloads to complete..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}// 發送退出信號并等待工作線程結束downloadQueue.quit();workerThread.join();std::cout << "Main thread exiting." << std::endl;return 0;
}
這個示例實現了一個簡單的后臺任務處理系統,主線程可以向隊列添加任務,而工作線程在后臺處理這些任務。這種模式在GUI應用、服務器程序等場景中很常見。
用戶界面響應性改進
多線程可以顯著提高用戶界面的響應性。下面是一個簡化的示例,演示如何在后臺線程執行耗時操作,同時保持主線程響應用戶輸入:
#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>// 模擬耗時計算
void heavyComputation(std::atomic<double>& progress, std::atomic<bool>& shouldStop) {for (int i = 0; i <= 100; ++i) {// 檢查是否應該停止if (shouldStop) {std::cout << "Computation cancelled!" << std::endl;return;}// 執行"計算"std::this_thread::sleep_for(std::chrono::milliseconds(100));// 更新進度progress = i;}std::cout << "Computation completed successfully!" << std::endl;
}// 顯示進度的線程
void displayProgress(const std::atomic<double>& progress, const std::atomic<bool>& shouldStop) {while (!shouldStop && progress < 100) {std::cout << "Progress: " << progress << "%" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}
}int main() {std::atomic<double> progress(0);std::atomic<bool> shouldStop(false);std::cout << "Starting heavy computation..." << std::endl;std::cout << "Press 'c' to cancel or any other key to check progress." << std::endl;// 啟動計算線程std::thread computationThread(heavyComputation, std::ref(progress), std::ref(shouldStop));// 啟動顯示進度的線程std::thread displayThread(displayProgress, std::ref(progress), std::ref(shouldStop));// 主線程處理用戶輸入char input;while (progress < 100 && !shouldStop) {input = std::cin.get();if (input == 'c' || input == 'C') {std::cout << "Cancellation requested." << std::endl;shouldStop = true;} else {std::cout << "Current progress: " << progress << "%" << std::endl;}}// 等待線程完成computationThread.join();displayThread.join();std::cout << "Program exiting." << std::endl;return 0;
}
在這個示例中,我們創建了兩個線程:一個執行耗時計算,另一個定期顯示進度。同時,主線程保持響應用戶輸入,允許用戶隨時取消計算。這種模式可以容易地擴展到實際的GUI應用程序中。
常見問題與注意事項
競態條件
當多個線程同時訪問共享數據,并且至少有一個線程修改數據時,就會發生競態條件(Race Condition):
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>// 全局計數器
int counter = 0;
std::mutex counterMutex; // 保護counter的互斥量// 不安全的遞增函數 - 存在競態條件
void incrementUnsafe(int numTimes) {for (int i = 0; i < numTimes; ++i) {++counter; // 競態條件!}
}// 安全的遞增函數 - 使用互斥量
void incrementSafe(int numTimes) {for (int i = 0; i < numTimes; ++i) {std::lock_guard<std::mutex> lock(counterMutex);++counter; // 受互斥量保護}
}int main() {int numThreads = 10;int incrementsPerThread = 100000;// 測試不安全的版本counter = 0;std::vector<std::thread> unsafeThreads;for (int i = 0; i < numThreads; ++i) {unsafeThreads.emplace_back(incrementUnsafe, incrementsPerThread);}for (auto& t : unsafeThreads) {t.join();}std::cout << "Unsafe counter value: " << counter << " (Expected: " << numThreads * incrementsPerThread << ")" << std::endl;// 測試安全的版本counter = 0;std::vector<std::thread> safeThreads;for (int i = 0; i < numThreads; ++i) {safeThreads.emplace_back(incrementSafe, incrementsPerThread);}for (auto& t : safeThreads) {t.join();}std::cout << "Safe counter value: " << counter << " (Expected: " << numThreads * incrementsPerThread << ")" << std::endl;return 0;
}
在不安全版本中,多個線程可能同時讀取counter的值,增加它,然后寫回,這可能導致某些遞增操作被覆蓋。安全版本使用互斥量確保每次只有一個線程可以修改counter,從而避免競態條件。
死鎖與活鎖
死鎖(Deadlock)是指兩個或多個線程互相等待對方持有的資源,導致所有線程都無法繼續執行:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mutexA;
std::mutex mutexB;// 可能導致死鎖的函數
void deadlockFunction1() {std::cout << "Thread 1 trying to lock mutexA..." << std::endl;std::lock_guard<std::mutex> lockA(mutexA);std::cout << "Thread 1 locked mutexA" << std::endl;// 添加延遲增加死鎖可能性std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Thread 1 trying to lock mutexB..." << std::endl;std::lock_guard<std::mutex> lockB(mutexB);std::cout << "Thread 1 locked mutexB" << std::endl;std::cout << "Thread 1 releasing both locks" << std::endl;
}void deadlockFunction2() {std::cout << "Thread 2 trying to lock mutexB..." << std::endl;std::lock_guard<std::mutex> lockB(mutexB);std::cout << "Thread 2 locked mutexB" << std::endl;// 添加延遲增加死鎖可能性std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Thread 2 trying to lock mutexA..." << std::endl;std::lock_guard<std::mutex> lockA(mutexA);std::cout << "Thread 2 locked mutexA" << std::endl;std::cout << "Thread 2 releasing both locks" << std::endl;
}// 安全版本,使用std::lock防止死鎖
void noDeadlockFunction1() {std::cout << "Safe Thread 1 trying to lock both mutexes..." << std::endl;std::scoped_lock lock(mutexA, mutexB); // C++17的std::scoped_lockstd::cout << "Safe Thread 1 locked both mutexes" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Safe Thread 1 releasing both locks" << std::endl;
}void noDeadlockFunction2() {std::cout << "Safe Thread 2 trying to lock both mutexes..." << std::endl;std::scoped_lock lock(mutexB, mutexA); // 注意順序不同,但不會導致死鎖std::cout << "Safe Thread 2 locked both mutexes" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Safe Thread 2 releasing both locks" << std::endl;
}int main() {// 示范死鎖(注意:這會使程序卡住)std::cout << "Demonstrating deadlock (program will hang):" << std::endl;/*std::thread t1(deadlockFunction1);std::thread t2(deadlockFunction2);t1.join();t2.join();*/// 展示避免死鎖的方法std::cout << "\nDemonstrating deadlock prevention:" << std::endl;std::thread t3(noDeadlockFunction1);std::thread t4(noDeadlockFunction2);t3.join();t4.join();return 0;
}
為避免死鎖:
- 始終以相同順序鎖定多個互斥量
- 使用
std::lock
或std::scoped_lock
同時鎖定多個互斥量 - 避免在持有鎖時調用用戶代碼(可能會嘗試獲取其他鎖)
- 使用層次鎖定,為每個互斥量分配層級,只允許按層級順序鎖定
活鎖(Livelock)類似于死鎖,但線程并非阻塞等待,而是持續嘗試某個無法完成的操作,導致CPU資源被消耗而無進展。
線程數量的選擇
選擇適當的線程數量對于優化性能至關重要:
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <numeric>
#include <algorithm>// 線程數量性能測試函數
void threadCountBenchmark(const std::vector<int>& data) {// 最大線程數為硬件并發線程數(通常是CPU核心數)unsigned int maxThreads = std::thread::hardware_concurrency();std::cout << "Hardware concurrency: " << maxThreads << " threads" << std::endl;// 測試不同線程數量for (unsigned int numThreads = 1; numThreads <= maxThreads * 2; numThreads += std::max(1u, maxThreads / 4)) {// 計算每個線程處理的元素數size_t blockSize = data.size() / numThreads;auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;std::vector<long long> partialSums(numThreads);// 創建線程for (unsigned int i = 0; i < numThreads; ++i) {size_t startIdx = i * blockSize;size_t endIdx = (i == numThreads - 1) ? data.size() : (i + 1) * blockSize;threads.emplace_back([&data, &partialSums, i, startIdx, endIdx](){// 模擬計算密集型任務long long sum = 0;for (size_t j = startIdx; j < endIdx; ++j) {sum += data[j] * data[j]; // 計算平方和}partialSums[i] = sum;});}// 等待所有線程完成for (auto& t : threads) {t.join();}// 合并結果long long totalSum = std::accumulate(partialSums.begin(), partialSums.end(), 0LL);auto end = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();std::cout << "Threads: " << numThreads << ", Time: " << duration << "ms"<< ", Result: " << totalSum << std::endl;}
}int main() {// 創建大量數據const size_t dataSize = 100'000'000;std::vector<int> data(dataSize);for (size_t i = 0; i < dataSize; ++i) {data[i] = i % 100; // 簡單模式}// 運行基準測試threadCountBenchmark(data);return 0;
}
選擇線程數量的一般指南:
- 對于計算密集型任務,線程數接近或等于CPU核心數通常是最優的
- 對于IO密集型任務,線程數可以超過CPU核心數,因為線程經常處于等待狀態
- 避免創建過多線程,這會增加線程切換開銷
- 考慮使用線程池來控制線程數量和重用線程
調試多線程程序
多線程程序的調試比單線程程序更具挑戰性,主要原因在于線程執行順序的不確定性。以下是一些有用的調試技巧:
- 使用線程ID標記日志
#include <iostream>
#include <thread>
#include <sstream>
#include <iomanip>
#include <mutex>std::mutex logMutex; // 保護日志輸出的互斥量// 帶線程ID的日志記錄函數
void log(const std::string& message) {std::lock_guard<std::mutex> lock(logMutex);std::ostringstream tid;tid << std::this_thread::get_id();std::cout << "[Thread " << std::setw(5) << tid.str() << "] " << message << std::endl;
}void workerFunction(int id) {log("Worker " + std::to_string(id) + " starting");std::this_thread::sleep_for(std::chrono::milliseconds(id * 100));log("Worker " + std::to_string(id) + " step 1");std::this_thread::sleep_for(std::chrono::milliseconds(id * 50));log("Worker " + std::to_string(id) + " finishing");
}int main() {log("Main thread starting");std::vector<std::thread> threads;for (int i = 0; i < 5; ++i) {threads.emplace_back(workerFunction, i + 1);}log("All workers started");for (auto& t : threads) {t.join();}log("All workers completed");return 0;
}
-
使用調試器的線程窗口:現代調試器如Visual Studio、GDB和LLDB都提供了線程窗口,可以查看所有線程的狀態并在線程之間切換。
-
使用條件編譯的調試幫助器:在關鍵點添加調試信息。
-
記錄時間戳:在日志中添加時間戳,幫助分析事件順序。
-
使用原子操作進行計數和檢查:使用原子變量跟蹤關鍵狀態轉換。
-
使用線程分析工具:如Intel Thread Checker、Valgrind的DRD和Helgrind工具等。
總結
在這篇文章中,我們介紹了C++11的std::thread
類及其基本用法,包括線程的創建、參數傳遞、生命周期管理以及常見問題。多線程編程是現代C++開發中不可或缺的一部分,掌握這些基礎知識將為你構建高性能、響應迅速的應用程序奠定基礎。
主要要點回顧:
-
線程創建與基本操作:使用
std::thread
創建線程,傳遞函數、函數對象或lambda表達式作為線程函數。 -
參數傳遞:使用值傳遞、
std::ref
引用傳遞或移動語義傳遞參數到線程函數。 -
線程管理:使用
join()
等待線程完成或detach()
允許線程在后臺運行。 -
線程本地存儲:使用
thread_local
關鍵字創建線程私有的變量。 -
異常處理:線程函數中的異常必須在線程內部捕獲,否則程序將終止。
-
線程安全問題:了解競態條件、死鎖等多線程編程常見問題,以及防范措施。
-
實際應用:使用多線程可以提高計算性能、改善用戶界面響應性、實現后臺任務處理等。
然而,本文只是多線程編程的開始。在接下來的文章中,我們將深入探討更多高級主題,如互斥量、鎖、條件變量等同步原語,它們對于構建線程安全的數據結構和算法至關重要。
這是我C++學習之旅系列的第五十四篇技術文章。查看完整系列目錄了解更多內容。