C++學習:六個月從基礎到就業——多線程編程:std::thread基礎

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;
}

多線程的優勢

多線程編程具有以下主要優勢:

  1. 提高性能:通過并行處理,多線程可以更有效地利用多核處理器,加速計算密集型任務。

  2. 響應性增強:在用戶界面應用中,使用獨立線程處理耗時操作可以保持界面響應迅速。

  3. 資源利用率提高:當一個線程等待I/O操作完成時,其他線程可以繼續執行,提高整體資源利用率。

  4. 簡化復雜問題:某些問題在多線程模型下更容易表達和理解。

多線程的挑戰

盡管多線程編程帶來諸多優勢,但也面臨以下挑戰:

  1. 同步問題:多線程訪問共享資源需要適當同步,否則會導致數據競爭和不確定行為。

  2. 死鎖風險:不當的線程同步可能導致死鎖,使程序永久卡住。

  3. 調試困難:多線程程序的執行具有不確定性,使得調試更加復雜。

  4. 可伸縮性問題:創建過多線程會導致線程切換開銷增加,反而降低性能。

  5. 設計復雜性:多線程程序的設計和實現通常比單線程程序更復雜。

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::refstd::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::moveunique_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;
}

為避免死鎖:

  1. 始終以相同順序鎖定多個互斥量
  2. 使用std::lockstd::scoped_lock同時鎖定多個互斥量
  3. 避免在持有鎖時調用用戶代碼(可能會嘗試獲取其他鎖)
  4. 使用層次鎖定,為每個互斥量分配層級,只允許按層級順序鎖定

活鎖(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;
}

選擇線程數量的一般指南:

  1. 對于計算密集型任務,線程數接近或等于CPU核心數通常是最優的
  2. 對于IO密集型任務,線程數可以超過CPU核心數,因為線程經常處于等待狀態
  3. 避免創建過多線程,這會增加線程切換開銷
  4. 考慮使用線程池來控制線程數量和重用線程

調試多線程程序

多線程程序的調試比單線程程序更具挑戰性,主要原因在于線程執行順序的不確定性。以下是一些有用的調試技巧:

  1. 使用線程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;
}
  1. 使用調試器的線程窗口:現代調試器如Visual Studio、GDB和LLDB都提供了線程窗口,可以查看所有線程的狀態并在線程之間切換。

  2. 使用條件編譯的調試幫助器:在關鍵點添加調試信息。

  3. 記錄時間戳:在日志中添加時間戳,幫助分析事件順序。

  4. 使用原子操作進行計數和檢查:使用原子變量跟蹤關鍵狀態轉換。

  5. 使用線程分析工具:如Intel Thread Checker、Valgrind的DRD和Helgrind工具等。

總結

在這篇文章中,我們介紹了C++11的std::thread類及其基本用法,包括線程的創建、參數傳遞、生命周期管理以及常見問題。多線程編程是現代C++開發中不可或缺的一部分,掌握這些基礎知識將為你構建高性能、響應迅速的應用程序奠定基礎。

主要要點回顧:

  1. 線程創建與基本操作:使用std::thread創建線程,傳遞函數、函數對象或lambda表達式作為線程函數。

  2. 參數傳遞:使用值傳遞、std::ref引用傳遞或移動語義傳遞參數到線程函數。

  3. 線程管理:使用join()等待線程完成或detach()允許線程在后臺運行。

  4. 線程本地存儲:使用thread_local關鍵字創建線程私有的變量。

  5. 異常處理:線程函數中的異常必須在線程內部捕獲,否則程序將終止。

  6. 線程安全問題:了解競態條件、死鎖等多線程編程常見問題,以及防范措施。

  7. 實際應用:使用多線程可以提高計算性能、改善用戶界面響應性、實現后臺任務處理等。

然而,本文只是多線程編程的開始。在接下來的文章中,我們將深入探討更多高級主題,如互斥量、鎖、條件變量等同步原語,它們對于構建線程安全的數據結構和算法至關重要。


這是我C++學習之旅系列的第五十四篇技術文章。查看完整系列目錄了解更多內容。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/80894.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80894.shtml
英文地址,請注明出處:http://en.pswp.cn/web/80894.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【計算機網絡】TCP如何保障傳輸可靠性_筆記

文章目錄 一、傳輸可靠性的6方面保障二、分段機制三、超時重傳機制四、流量控制五、擁塞控制 提示&#xff1a;以下是本篇文章正文內容&#xff0c;下面案例可供參考 源網站 按TCP/IP 4層體系&#xff0c;TCP位于傳輸層&#xff0c;為應用層提供服務 一、傳輸可靠性的6方面保障…

2025年保姆級教程:Powershell命令補全、主題美化、文件夾美化及Git擴展

文章目錄 1. 美化 Powershell 緣起2. 安裝 oh-my-posh 和 posh-git3. 安裝文件夾美化主題【可選】 1. 美化 Powershell 緣起 背景&#xff1a;用了 N 年的 Windows 系統突然覺得命令行實在太難用了&#xff0c;沒有補全功能、界面也不美觀。所以&#xff0c;我決定改變它。但是…

基于Mongodb的分布式文件存儲實現

分布式文件存儲的方案有很多&#xff0c;今天分享一個基于mongodb數據庫來實現文件的存儲&#xff0c;mongodb支持分布式部署&#xff0c;以此來實現文件的分布式存儲。 基于 MongoDB GridFS 的分布式文件存儲實現&#xff1a;從原理到實戰 一、引言 當系統存在大量的圖片、…

【Linux】Linux安裝并配置Redis

目錄 1.安裝 2.啟動服務 3.配置 3.1.綁定地址 3.2.保護模式 3.3.持久化選項 3.3.1.RDB 持久化 3.3.2.AOF 持久化 3.3.3.如何選擇 1.安裝 Redis 可以從默認的 CentOS 軟件倉庫中安裝。運行以下命令來安裝 Redis sudo dnf install redis -y 響應如下 2.啟動服務 安裝完成后&…

python-數據可視化(大數據、數據分析、可視化圖像、HTML頁面)

通過 Python 讀取 XLS 、CSV文件中的數據&#xff0c;對數據進行處理&#xff0c;然后生成包含柱狀圖、扇形圖和折線圖的 HTML 報告。這個方案使用了 pandas 處理數據&#xff0c;matplotlib 生成圖表&#xff0c;并將圖表嵌入到 HTML 頁面中。 1.XSL文件生成可視化圖像、生成h…

黑馬點評相關知識總結

黑馬點評的項目總結 主要就黑馬點評項目里面的一些比較重要部分的一次總結&#xff0c;方便以后做復習。 基于Session實現短信登錄 短信驗證碼登錄 這部分使用常規的session來存儲用戶的登錄狀態&#xff0c;其中短信發送采取邏輯形式&#xff0c;并不配置云服務驗證碼功能。…

手搓四人麻將程序

一、麻將牌的表示 在麻將游戲中&#xff0c;總共有一百四十四張牌&#xff0c;這些牌被分為多個類別&#xff0c;每個類別又包含了不同的牌型。具體來說&#xff0c;麻將牌主要包括序數牌、字牌和花牌三大類。序數牌中&#xff0c;包含有萬子、條子和筒子&#xff0c;每種花色…

【Java高階面經:數據庫篇】17、分庫分表分頁查詢優化:告別慢查詢與內存爆炸

一、分庫分表基礎&#xff1a;策略與中間件形態 1.1 分庫分表核心策略 分庫分表是應對海量數據存儲和高并發訪問的關鍵架構設計&#xff0c;其核心在于將數據分散到不同的數據庫或表中&#xff0c;以突破單庫單表的性能限制。常見的分庫分表策略包括&#xff1a; 1.1.1 哈希…

貪心算法之跳躍游戲問題

問題背景 本文背景是leetcode的一道經典題目&#xff1a;跳躍游戲&#xff0c;描述如下&#xff1a; 給定一個非負整數數組 nums&#xff0c;初始位于數組的第一個位置&#xff08;下標0&#xff09;。數組中的每個元素表示在該位置可以跳躍的最大長度。判斷是否能夠到達最后…

Label Studio:開源標注神器

目錄 一、Label Studio 是什么&#xff1f; 二、核心功能大揭秘 2.1 多類型數據全兼容 2.2 個性化定制隨心配 2.3 團隊協作超給力 2.4 機器學習巧集成 三、上手實操超簡單 3.1 安裝部署不頭疼 3.1.1 Docker安裝 3.1.2 pip安裝 3.1.3 Anaconda安裝 3.2 快速開啟標注…

創建信任所有證書的HttpClient:Java 實現 HTTPS 接口調用,等效于curl -k

在 Java 生態中&#xff0c;HttpClient 和 Feign 都是調用第三方接口的常用工具&#xff0c;但它們的定位、設計理念和使用場景有顯著差異。以下是詳細對比&#xff1a; DIFF1. 定位與抽象層級 特性HttpClientFeign層級底層 HTTP 客戶端庫&#xff08;處理原始請求/響應&#…

從零基礎到最佳實踐:Vue.js 系列(7/10):《常用內置 API 與插件》

引言 Vue.js 是一款輕量且強大的前端框架&#xff0c;因其易用性和靈活性受到廣泛歡迎。無論是初學者還是資深開發者&#xff0c;都可以通過其內置 API 和插件生態快速構建高效、可維護的 Web 應用。本文將從基礎用法講起&#xff0c;逐步深入到進階技巧&#xff0c;結合大量實…

線性代數:AI大模型的數學基石

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家、CSDN平臺優質創作者&#xff0c;高級開發工程師&#xff0c;數學專業&#xff0c;10年以上C/C, C#, Java等多種編程語言開發經驗&#xff0c;擁有高級工程師證書&#xff1b;擅長C/C、C#等開發語言&#xff0c;熟悉Java常用開…

Java-System工具類深度解析

Java-System工具類深度解析 前言一、System 類概述1.1 基本定義與特點1.2 重要成員變量 二、標準輸入輸出功能2.1 標準輸入&#xff08;System.in&#xff09;2.2 標準輸出&#xff08;System.out&#xff09;2.3 標準錯誤輸出&#xff08;System.err&#xff09; 三、系統屬性…

刪除用戶憑證

Git 部分倉庫無法操作&#xff0c;部分倉庫沒問題 問題出現 我用個人電腦修改了項目&#xff0c;提交了git。然后第二天在公司電腦git pull的時候失敗&#xff0c;只有部分倉庫&#xff0c;git colne直接失敗&#xff0c;部分倉庫無問題。 解決方式 刪除git相關憑證&#xff…

19. 結合Selenium和YAML對頁面實例化PO對象改造

19. 結合Selenium和YAML對頁面實例化PO對象改造 一、架構升級核心思路 1.1 改造核心目標 # 原始PO模式&#xff1a;顯式定義元素定位 username (id, ctl00_MainContent_username)# 改造后PO模式&#xff1a;動態屬性訪問 self.username.send_keys(Tester) # 自動觸發元素定…

鴻蒙App開發學習路徑

以下是一份系統的鴻蒙&#xff08;HarmonyOS&#xff09;App開發學習路徑&#xff0c;適合從零開始逐步掌握相關技能&#xff1a; 1. 基礎知識儲備 1.1 理解鴻蒙系統 鴻蒙核心特性&#xff1a;分布式能力、一次開發多端部署、原子化服務、ArkUI框架。與Android/iOS的區別&…

spring boot啟動報錯:2002 - Can‘t connect to server on ‘192.168.10.212‘ (10061)

錯誤代碼 10061 通常表明無法建立到指定服務器的網絡連接。這個錯誤屬于 Windows Sockets 錯誤代碼&#xff0c;具體指的是無法建立網絡連接&#xff0c;通常是因為目標地址不可達。以下是一些解決此問題的步驟&#xff1a; 檢查 IP 地址和端口&#xff1a; 確保你輸入的 IP …

ARMv7的NVIC中斷優先級

1. 優先級模型 數值規則:數值越小,優先級越高(例如優先級0的異常比優先級1的異常更高);若多個異常的優先級相同,則 異常號(Exception Number) 較小的異常優先執行。固定優先級異常(不可配置):異常類型 優先級值 說明 Reset -3 最高優先級(系統復位) NMI -2 不可屏…

gitee錯誤處理總結

背景 如上圖&#xff0c;根據圖片中的 Git 錯誤提示&#xff0c;我們遇到的問題是 ?本地分支落后于遠程分支&#xff0c;導致 git push 被拒絕。 ?問題原因? 遠程倉庫的 master 分支有其他人推送的新提交&#xff0c;而您的本地 master 分支未同步這些更新&#xff08;即本…