玩轉 C++11 多線程:讓你的程序飛起來的 std::thread 終極指南
前言:為啥要學多線程?
想象一下,你正在廚房做飯。如果你是單線程工作,那就只能先切菜,切完再炒菜,炒完再煮湯...一項一項按順序來。但現實中的你肯定是多線程操作啊:鍋里炒著菜,同時旁邊的電飯煲在煮飯,熱水壺在燒水,也許你還能同時看看手機...這就是多線程的威力!
在程序世界里,多線程就像多了幾個"分身",可以同時處理不同的任務,充分利用多核CPU的性能,讓程序跑得飛快。特別是現在誰的電腦不是多核啊,不用多線程簡直是浪費資源!
C++11標準終于給我們帶來了官方的多線程支持——std::thread,從此不用再依賴操作系統特定的API或第三方庫,寫多線程程序方便多了!
第一步:創建你的第一個線程
好,閑話少說,直接上代碼看看怎么創建一個線程:
#include <iostream>
#include <thread>
// 這是我們要在新線程中執行的函數
void hello_thread() {
std::cout << "哈嘍,我是一個新線程!" << std::endl;
}
int main() {
// 創建一個執行hello_thread函數的線程
std::thread t(hello_thread);
// 主線程打個招呼
std::cout << "主線程:我正在等一個線程干活..." << std::endl;
// 等待線程完成
t.join();
std::cout << "所有線程都結束了,程序退出!" << std::endl;
return 0;
}
輸出結果可能是:
主線程:我正在等一個線程干活...
哈嘍,我是一個新線程!
所有線程都結束了,程序退出!
或者是:
哈嘍,我是一個新線程!
主線程:我正在等一個線程干活...
所有線程都結束了,程序退出!
咦?為啥輸出順序不固定?因為兩個線程是并發執行的,誰先打印完全看 CPU 的心情!這就是多線程的特點——不確定性。
代碼解析:
- 創建線程超簡單,就一行代碼:std::thread t(hello_thread);。線程一創建就立刻開始執行了。
- t.join() 是啥意思呢?它相當于說:"主線程,你等等這個新線程,等它干完活再繼續"。如果沒有這行,主線程可能提前結束,程序就崩潰了!
給線程傳參數
線程不能只會喊"哈嘍"吧?我們得給它點實際任務,還得告訴它一些參數。傳參數超簡單:
#include <iostream>
#include <thread>
#include <string>
void greeting(std::string name, int times) {
for (int i = 0; i < times; i++) {
std::cout << "你好," << name << "!這是第 " << (i+1) << " 次問候!" << std::endl;
}
}
int main() {
// 創建線程并傳遞參數
std::thread t(greeting, "張三", 3);
std::cout << "主線程:我讓線程去問候張三了..." << std::endl;
// 等待線程完成
t.join();
std::cout << "問候完畢!" << std::endl;
return 0;
}
輸出結果:
主線程:我讓線程去問候張三了...
你好,張三!這是第 1 次問候!
你好,張三!這是第 2 次問候!
你好,張三!這是第 3 次問候!
問候完畢!
傳參就像普通函數調用一樣,直接在線程構造函數后面加參數就行。但是有個坑:參數是"拷貝"到線程中的,所以小心對象的復制開銷!
用Lambda表達式創建線程
每次都要單獨寫個函數太麻煩了,有沒有簡單方法?有啊,用Lambda表達式!
#include <iostream>
#include <thread>
int main() {
// 使用Lambda表達式創建線程
std::thread t([]() {
std::cout << "我是Lambda創建的線程,帥不帥?" << std::endl;
for (int i = 5; i > 0; i--) {
std::cout << "倒計時: " << i << std::endl;
}
});
std::cout << "主線程:Lambda線程正在倒計時..." << std::endl;
t.join();
std::cout << "倒計時結束!" << std::endl;
return 0;
}
Lambda表達式就像一個臨時小函數,用完就扔,方便得很!特別適合那種只用一次的簡單邏輯。
多線程通信的坑:數據競爭
多線程編程最大的坑就是多個線程同時訪問同一數據時會出現"數據競爭"。來看個例子:
#include <iostream>
#include <thread>
#include <vector>
int counter = 0; // 共享的計數器
void increment_counter(int times) {
for (int i = 0; i < times; i++) {
counter++; // 危險操作!多線程同時修改
}
}
int main() {
std::vector<std::thread> threads;
// 創建5個線程,每個線程將counter增加10000次
for (int i = 0; i < 5; i++) {
threads.push_back(std::thread(increment_counter, 10000));
}
// 等待所有線程完成
for (auto& t : threads) {
t.join();
}
std::cout << "理論上counter應該等于:" << 5 * 10000 << std::endl;
std::cout << "實際上counter等于:" << counter << std::endl;
return 0;
}
輸出可能是:
理論上counter應該等于:50000
實際上counter等于:42568
咦?怎么少了那么多?因為 counter++ 看起來是一條語句,但實際上分三步:讀取counter的值、加1、寫回counter。當多個線程同時執行這個操作,就會互相"踩踏",導致最終結果小于預期。
這就是臭名昭著的數據競爭問題,解決方法有互斥鎖、原子操作等,后面會講。
線程管理的基本操作
(1) join() - 等待線程完成
我們已經見過 join() 了,它會阻塞當前線程,直到目標線程執行完畢。
std::thread t(some_function);
t.join(); // 等待t完成
(2) detach() - 讓線程"自生自滅"
有時候,我們啟動一個線程后不想等它了,可以用 detach() 讓它獨立運行:
std::thread t(background_task);
t.detach(); // 線程在后臺獨立運行
std::cout << "主線程不管子線程了,繼續自己的事" << std::endl;
detach后的線程稱為"分離線程"或"守護線程",它會在后臺默默運行,直到自己的任務完成。但要小心:如果主程序結束了,這些分離線程會被強制終止!
(3) joinable() - 檢查線程是否可等待
在join之前,最好檢查一下線程是否可以被等待:
std::thread t(some_function);
// ... 一些代碼 ...
if (t.joinable()) {
t.join();
}
這避免了對已經 join 或 detach 過的線程再次操作,否則會崩潰。
防止忘記join:RAII風格的線程包裝器
C++的經典模式:用對象的生命周期管理資源。我們可以創建一個線程包裝器,在析構時自動join:
#include <iostream>
#include <thread>
class thread_guard {
private:
std::thread& t;
public:
// 構造函數,接收線程引用
explicit thread_guard(std::thread& t_) : t(t_) {}
// 析構函數,自動join線程
~thread_guard() {
if (t.joinable()) {
t.join();
}
}
// 禁止復制
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
void some_function() {
std::cout << "線程工作中..." << std::endl;
}
int main() {
std::thread t(some_function);
thread_guard g(t); // 創建守衛對象
// 即使這里拋出異常,thread_guard的析構函數也會被調用,確保t被join
std::cout << "主線程繼續工作..." << std::endl;
return 0; // 函數結束,g被銷毀,自動調用t.join()
}
這樣即使發生異常,或者開發者忘記手動join,線程也會被正確等待,避免程序崩潰。
線程間的互斥:mutex
前面說到數據競爭問題,最常用的解決方案是互斥鎖(mutex):
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
int counter = 0;
std::mutex counter_mutex; // 保護counter的互斥鎖
void safe_increment(int times) {
for (int i = 0; i < times; i++) {
counter_mutex.lock(); // 鎖定互斥鎖
counter++; // 安全操作
counter_mutex.unlock(); // 解鎖
}
}
int main() {
std::vector<std::thread> threads;
// 創建5個線程
for (int i = 0; i < 5; i++) {
threads.push_back(std::thread(safe_increment, 10000));
}
// 等待所有線程
for (auto& t : threads) {
t.join();
}
std::cout << "現在counter正確等于:" << counter << std::endl;
return 0;
}
輸出:
現在counter正確等于:50000
太好了!結果正確了。但這樣手動lock/unlock很容易出錯,如果忘記unlock或者發生異常,就會死鎖。所以更推薦使用RAII風格的std::lock_guard:
void better_safe_increment(int times) {
for (int i = 0; i < times; i++) {
std::lock_guard<std::mutex> lock(counter_mutex); // 自動鎖定和解鎖
counter++;
}
}
lock_guard在構造時鎖定互斥鎖,在析構時自動解鎖,無論是正常退出還是異常退出都能保證互斥鎖被釋放。
高級話題:條件變量
線程間的同步不只有互斥,有時我們需要一個線程等待某個條件滿足。條件變量就是干這個的:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> data_queue; // 共享的數據隊列
std::mutex queue_mutex;
std::condition_variable data_cond;
// 生產者線程
void producer() {
for (int i = 0; i < 5; i++) {
{
std::lock_guard<std::mutex> lock(queue_mutex);
data_queue.push(i); // 添加數據
std::cout << "生產了數據: " << i << std::endl;
} // 鎖在這里釋放
data_cond.notify_one(); // 通知一個等待的消費者
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 稍微等一下
}
}
// 消費者線程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mutex);
// 等待隊列有數據(避免虛假喚醒)
data_cond.wait(lock, [] { return !data_queue.empty(); });
// 取出并處理數據
int value = data_queue.front();
data_queue.pop();
std::cout << "消費了數據: " << value << std::endl;
if (value == 4) break; // 收到最后一個數據后退出
}
}
int main() {
std::thread prod(producer);
std::thread cons(consumer);
prod.join();
cons.join();
std::cout << "所有數據都生產和消費完畢!" << std::endl;
return 0;
}
這個例子展示了經典的"生產者-消費者"模式:生產者往隊列里放數據,消費者從隊列里取數據。條件變量確保消費者不會在隊列為空時嘗試取數據。
線程與異常安全
在多線程程序中處理異常尤為重要。如果線程執行時拋出異常,且沒被捕獲,整個程序會直接崩潰!以下是安全處理方式:
#include <iostream>
#include <thread>
#include <exception>
void function_that_throws() {
throw std::runtime_error("故意拋出的異常!");
}
void thread_function() {
try {
function_that_throws();
} catch (const std::exception& e) {
std::cout << "線程捕獲到異常: " << e.what() << std::endl;
}
}
int main() {
std::thread t(thread_function);
t.join();
std::cout << "程序正常結束" << std::endl;
return 0;
}
輸出:
線程捕獲到異常: 故意拋出的異常!
程序正常結束
記住:每個線程都有自己獨立的調用棧,異常不會跨線程傳播!在哪個線程拋出,就必須在哪個線程捕獲。
實用技巧
(1) 獲取線程ID
每個線程都有唯一的ID,用于標識:
#include <iostream>
#include <thread>
void print_id() {
std::cout << "線程ID: " << std::this_thread::get_id() << std::endl;
}
int main() {
std::thread t1(print_id);
std::thread t2(print_id);
std::cout << "主線程ID: " << std::this_thread::get_id() << std::endl;
std::cout << "t1的ID: " << t1.get_id() << std::endl;
std::cout << "t2的ID: " << t2.get_id() << std::endl;
t1.join();
t2.join();
return 0;
}
(2) 線程休眠
有時需要讓線程暫停一會兒:
#include <iostream>
#include <thread>
#include <chrono>
void sleepy_thread() {
std::cout << "我要睡覺了..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "睡醒了!" << std::endl;
}
int main() {
std::thread t(sleepy_thread);
t.join();
return 0;
}
(3) 獲取CPU核心數
為了根據CPU核心優化線程數量:
#include <iostream>
#include <thread>
int main() {
unsigned int num_cores = std::thread::hardware_concurrency();
std::cout << "你的CPU有 " << num_cores << " 個硬件線程(核心)" << std::endl;
// 根據核心數創建線程
unsigned int num_threads = num_cores;
std::cout << "將創建 " << num_threads << " 個線程以充分利用CPU" << std::endl;
return 0;
}
實際案例:并行圖像處理
來個實際應用案例:用多線程加速圖像處理。這里我們簡化為操作一個二維數組:
#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <chrono>
// 模擬圖像處理函數
void process_image_part(std::vector<std::vector<int>>& image, int start_row, int end_row) {
for (int i = start_row; i < end_row; i++) {
for (int j = 0; j < image[i].size(); j++) {
// 模擬復雜處理,例如圖像模糊
image[i][j] = (image[i][j] + 10) * 2;
// 模擬耗時操作
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
}
}
int main() {
// 創建模擬圖像 (1000x1000)
std::vector<std::vector<int>> image(1000, std::vector<int>(1000, 5));
// 獲取CPU核心數
unsigned int num_cores = std::thread::hardware_concurrency();
unsigned int num_threads = num_cores; // 使用和核心數一樣多的線程
std::cout << "使用 " << num_threads << " 個線程處理圖像..." << std::endl;
// 開始計時
auto start_time = std::chrono::high_resolution_clock::now();
// 創建線程并分配工作
std::vector<std::thread> threads;
int rows_per_thread = image.size() / num_threads;
for (unsigned int i = 0; i < num_threads; i++) {
int start_row = i * rows_per_thread;
int end_row = (i == num_threads - 1) ? image.size() : (i + 1) * rows_per_thread;
threads.push_back(std::thread(process_image_part, std::ref(image), start_row, end_row));
}
// 等待所有線程完成
for (auto& t : threads) {
t.join();
}
// 結束計時
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "圖像處理完成!耗時: " << duration.count() << " 毫秒" << std::endl;
// 驗證結果(只顯示部分)
std::cout << "處理后的圖像樣本(左上角): " << std::endl;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
std::cout << image[i][j] << " ";
}
std::cout << std::endl;
}
return 0;
}
這個例子展示了如何將大型任務分解成多個小塊,分配給多個線程并行處理,充分利用多核CPU的優勢。
多線程的優秀實踐
- 保持簡單:多線程代碼難以調試,盡量簡化每個線程的工作。
- 避免共享狀態:盡可能減少線程間共享的數據,以降低同步復雜度。
- 適當的線程數量:通常等于或略多于CPU核心數,太多反而會因為頻繁切換導致性能下降。
- 使用高級抽象:考慮使用std::async、std::future或線程池,而不是直接管理線程。
- 測試和調試:在各種條件下測試多線程代碼,包括高負載和邊緣情況。
結語
從此,你已經掌握了C++11多線程編程的基礎知識!從創建線程到傳遞參數,從互斥鎖到條件變量,從簡單示例到實際應用。多線程編程確實比單線程復雜,但掌握了這些技能,你就能寫出更高效、響應更快的程序。
記住,多線程編程需要實踐和耐心。開始時可能會遇到各種莫名其妙的問題,但隨著經驗積累,你會越來越熟練。不妨從簡單的多線程程序開始,逐步挑戰更復雜的場景。
最后的建議:寫多線程程序時,時刻保持清醒和警惕,因為多線程bug可能是最難調試的bug之一!
愿你的多線程之旅愉快且充滿成就感!