C++并發(fā)編程簡史:一段你不得不知道的傳奇
嘿,讓我們來聊聊 C++ 并發(fā)編程的精彩旅程吧!?? 想象一下,在 1998 年那個"單線程時代",C++ 就像個固執(zhí)的獨行俠,完全不懂多線程的魅力。開發(fā)者們只能依賴各種系統(tǒng)專屬的 API,就像在用方言交流一樣難懂 ??。
后來,像 Boost.Thread 這樣的"翻譯官"??? 出現(xiàn)了,讓并發(fā)編程變得優(yōu)雅了不少。但真正的轉(zhuǎn)折點是 2011 年的 C++11 標準,它給 C++ 裝上了"并發(fā)引擎"???,帶來了std::thread、std::mutex 等超強工具。
C++17 和 C++20 更是錦上添花,帶來了shared_mutex、協(xié)程等炫酷特性 ???纯船F(xiàn)在的 C++,簡直就像變了個人似的,讓我們能優(yōu)雅地駕馭多核時代的浪潮!??
C++多線程的前世今生
啊,讓我們坐上時光機,回到1998年 ?。那時的C++還是個"單線程主義者" - 它根本不承認多線程的存在!就像一個固執(zhí)的老頑固,堅持"一次只做一件事"。不僅如此,它連個像樣的內(nèi)存模型都沒有。程序員們想寫多線程程序?抱歉,除非你愿意依賴編譯器特定的擴展... ??
但是呢,編譯器廠商們可不這么想。他們看到了程序員們渴望多線程的眼神 ??。于是乎,他們開始通過各種方式支持多線程。Windows程序員有了Windows API,Unix黨有了POSIX線程庫。雖然這些支持比較基礎(基本就是把C的API搬過來),但是好歹能用不是? ??
來看個具體例子。假設我們想寫個簡單的多線程程序,在Windows下可能是這樣:
#include <windows.h>
#include <stdio.h>
// ?? 線程函數(shù) - 就像一個獨立的小工人
DWORD WINAPI PrintHello(LPVOID lpParam) {
printf("Hello from Windows thread!\n"); // ?? 打個招呼
return0; // ? 工作完成,安全退出
}
int main() {
// ?? 創(chuàng)建新線程 - 就像開啟一條新的生產(chǎn)線
HANDLE hThread = CreateThread(
NULL, // ?? 默認安全屬性
0, // ?? 默認棧大小
PrintHello, // ???? 指定工人要做的工作
NULL, // ?? 沒有參數(shù)傳遞
0, // ?? 立即啟動線程
NULL // ??? 不需要線程ID
);
// ? 等待線程完成 - 像等待工人完成工作
WaitForSingleObject(hThread, INFINITE);
// ?? 清理線程句柄 - 收拾好工作臺
CloseHandle(hThread);
return0; // ?? 主程序圓滿完成
}
而在POSIX系統(tǒng)上,同樣的程序要這么寫:
#include <pthread.h> // ?? POSIX 線程庫頭文件
#include <stdio.h> // ?? 標準輸入輸出
// ?? 線程執(zhí)行函數(shù) - 就像一個獨立的工作者
void* PrintHello(void* arg) {
printf("Hello from POSIX thread!\n"); // ?? 打個招呼
returnNULL; // ? 工作完成,安全返回
}
int main() {
pthread_t thread; // ?? 聲明線程變量,像是工人的工牌
// ?? 創(chuàng)建并啟動新線程
// 參數(shù)分別是:
// 1?? 線程標識符的指針
// 2?? 線程屬性(NULL表示使用默認屬性)
// 3?? 線程將要執(zhí)行的函數(shù)
// 4?? 傳遞給線程函數(shù)的參數(shù)
pthread_create(&thread, NULL, PrintHello, NULL);
// ? 等待線程完成 - 就像等待工人干完活
pthread_join(thread, NULL);
return0; // ?? 主程序圓滿結(jié)束
}
看到?jīng)]? 同樣的功能,兩種完全不同的寫法! 這簡直就像是在寫兩種不同的語言。
但是C++程序員們可不滿足于此。他們想要更優(yōu)雅的解決方案! 于是像MFC、Boost這樣的庫橫空出世了 ??♂?。這些庫把底層的API包裝得漂漂亮亮的,還加入了很多實用的功能。比如說,使用Boost.Thread的話,上面的代碼就可以寫成這樣:
#include <boost/thread.hpp> // ?? Boost的線程庫
#include <iostream> // ?? 輸入輸出流
// ?? 線程要執(zhí)行的任務函數(shù)
void PrintHello() {
std::cout << "Hello from Boost thread!" << std::endl; // ?? 打個招呼
}
int main() {
// ?? 創(chuàng)建并啟動新線程
boost::thread t(PrintHello); // ?? 線程開始執(zhí)行
t.join(); // ? 等待線程完成
return0; // ?? 主程序結(jié)束
}
是不是清爽多了? ?? 特別是它引入了RAII(資源獲取即初始化)的概念,讓互斥鎖的使用變得更安全。就像這樣:
boost::mutex mtx; // ?? 創(chuàng)建一個互斥鎖 - 就像一把神奇的鎖
{
// ?? RAII方式加鎖 - 進入?yún)^(qū)域自動上鎖
boost::mutex::scoped_lock lock(mtx);
// ?? 這里是受保護的代碼區(qū)域
// ?? 只有一個線程能在同一時間進入這里
// ?? 可以安全地訪問共享資源
// ?? 比如修改共享數(shù)據(jù)...
} // ?? 離開作用域時自動解鎖 - 非常優(yōu)雅且安全!
// ??? 即使發(fā)生異常也能確保解鎖
但是呢,這些庫再好,終究不是語言標準的一部分??缙脚_時還是會遇到各種奇怪的問題 ??。直到C++11的出現(xiàn),這個問題才得到了徹底的解決。但這個,就是另外一個精彩的故事了... ?
這段歷史告訴我們什么呢?它讓我們看到了C++社區(qū)的創(chuàng)造力和適應力。即使在標準不支持的情況下,依然找到了方法來滿足多線程編程的需求。這種精神,才是真正讓C++成為一門偉大語言的原因啊! ??
C++11: 多線程的春天來了!
2011年,C++終于迎來了期待已久的官方多線程支持! 就像給C++裝上了一臺"并發(fā)引擎" ???。來看看這個全新的世界吧:
#include <iostream> // ?? 標準輸入輸出流
#include <thread> // ?? 線程支持
#include <mutex> // ?? 互斥鎖支持
// ??? 保護std::cout的互斥鎖,防止輸出混亂
std::mutex cout_mutex;
// ???? 咖啡師的工作流程
void MakeCoffee() {
std::lock_guard<std::mutex> lock(cout_mutex); // ?? 自動加鎖解鎖,很安全!
std::cout << "? 正在煮咖啡..." << std::endl; // ?? 安全地輸出信息
}
// ???? 茶師的工作流程
void MakeTea() {
std::lock_guard<std::mutex> lock(cout_mutex); // ?? 獲取輸出權限
std::cout << "?? 正在泡茶..." << std::endl; // ?? 安全地輸出信息
}
int main() {
// ?? 開啟兩條并行的工作流水線
std::thread coffee_master(MakeCoffee); // ?? 啟動咖啡師的線程
std::thread tea_master(MakeTea); // ?? 啟動茶師的線程
// ? 等待兩位師傅完成他們的工作
coffee_master.join(); // ?? 等待咖啡師
tea_master.join(); // ?? 等待茶師
return0; // ?? 圓滿完成!
}
看到了嗎?這就是現(xiàn)代C++的魅力! ?? 不需要再記那些晦澀的API了,一切都變得如此自然。就像用中文寫代碼一樣順暢~
而且C++11不只是提供了基礎的線程支持,它還給了我們一整套并發(fā)工具箱!
比如說,如果我們想讓咖啡師和茶師輪流工作,可以用條件變量:
首先是基本的頭文件和全局變量設置:
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
// ?? 我們需要這些工具來協(xié)調(diào)咖啡師和茶師的工作
std::mutex mtx; // ?? 互斥鎖:就像休息室的門鎖
std::condition_variable cv; // ?? 條件變量:像是咖啡師和茶師之間的對講機
bool coffee_ready = false; // ?? 狀態(tài)標志:咖啡完成的信號
接下來是咖啡師的工作流程:
void BaristaMakeCoffee() {
// 第一步:宣布開始工作 ??
{
std::lock_guard<std::mutex> lock(mtx); // ?? 先鎖門
std::cout << "咖啡師: 開始煮咖啡..." << std::endl;
} // ?? 自動解鎖,其他人可以用輸出了
// 第二步:認真煮咖啡 ??
std::this_thread::sleep_for(std::chrono::seconds(2)); // ? 煮咖啡需要時間
// 第三步:完成并通知茶師 ??
{
std::lock_guard<std::mutex> lock(mtx);
coffee_ready = true; // ?? 設置完成標志
std::cout << "咖啡師: 咖啡準備好了! ??" << std::endl;
}
cv.notify_one(); // ?? 給茶師發(fā)消息
}
然后是茶師的工作流程:
void TeaMasterWaitAndMakeTea() {
// 第一步:等待咖啡師的信號 ??
{
std::unique_lock<std::mutex> lock(mtx); // ?? 特殊的鎖,可以被條件變量解開
cv.wait(lock, [] { return coffee_ready; }); // ?? 等待咖啡完成信號
}
// 第二步:開始泡茶 ??
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "茶師: 咖啡好了,我開始泡茶..." << std::endl;
}
std::this_thread::sleep_for(std::chrono::seconds(1)); // ?? 泡茶也需要時間
// 第三步:完成泡茶 ?
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "茶師: 茶也準備好了! ??" << std::endl;
}
}
最后是主程序的協(xié)調(diào)部分:
int main() {
std::cout << "?? 咖啡廳開始營業(yè)..." << std::endl;
// ?? 安排兩位師傅開始工作
std::thread barista(BaristaMakeCoffee); // ???? 咖啡師上崗
std::thread tea_master(TeaMasterWaitAndMakeTea); // ???? 茶師上崗
// ?? 等待工作完成
barista.join(); // ?? 等咖啡師完成
tea_master.join(); // ?? 等茶師完成
std::cout << "?? 今天的飲品都準備完成啦!" << std::endl;
return 0;
這不就是現(xiàn)實生活中的場景嗎? 茶師要等咖啡師完成才開始工作,多么和諧的工作流程! ??
- C++11還引入了很多其他好用的工具:
- std::async 和std::future 用于異步任務 ??
- std::atomic 用于原子操作 ??
各種同步原語(互斥鎖、條件變量、信號量等) ??
最重要的是,這些工具都是標準庫的一部分,意味著你的代碼可以在任何支持C++11的平臺上運行! 再也不用擔心跨平臺問題了~ ??
C++17與C++20: 并發(fā)編程的新篇章
哇!讓我們一起來看看C++17和C++20在并發(fā)編程方面帶來的超級大禮包吧!
還記得以前處理多個互斥鎖時那種提心吊膽的感覺嗎?生怕搞出死鎖來 ???,F(xiàn)在好啦!C++17給我們帶來了超級實用的scoped_lock!它就像一個聰明的管家 ??,自動幫我們按照正確的順序處理多個鎖,再也不用擔心死鎖啦!
// 看看這個超級管家是怎么工作的 ??
std::mutex m1, m2, m3; // 三把小鎖 ??
{
std::scoped_lock locks(m1, m2, m3); // 交給管家,一切都搞定!
// 安心寫代碼... ??
} // 管家會自動幫我們解鎖,貼心! ?
C++17還給我們帶來了shared_mutex - 這簡直就是給讀寫操作開了個派對! ?? 多個讀者可以一起蹦迪,但寫者需要包場獨舞~ 這不就是傳說中的"共享-獨占"模式嘛!
std::shared_mutex party_room; ??
// 讀者們可以一起嗨! ????
{
std::shared_lock<std::shared_mutex> group_entry(party_room);
// 大家一起讀數(shù)據(jù),熱鬧! ??
}
// 寫者需要包場 ??
{
std::unique_lock<std::shared_mutex> vip_entry(party_room);
// 獨自修改數(shù)據(jù),安靜... ??
}
到了C++20,簡直就是開了掛! ?? 它帶來了jthread(智能線程)、閉鎖、屏障、信號量這些厲害角色!特別是jthread,它就像是給普通線程裝上了自動駕駛系統(tǒng) ??,不用手動join,還能隨時喊停!
// 來看看這個智能線程有多聰明 ??
void future_threads() {
std::jthread smart_worker([](std::stop_token stoken) {
while (!stoken.stop_requested()) { // 隨時準備停車! ??
// 干活ing... ????
}
});
// 不用管它,下班自己會收工! ??
}
最讓人興奮的是協(xié)程的加入! ?? 它就像給你的代碼加上了任意門,可以隨時暫停、繼續(xù),玩出各種花樣!比如這個生成斐波那契數(shù)列的協(xié)程,簡直優(yōu)雅得不要不要的~ ?
generator<int> fibonacci() { // 數(shù)學界的魔術師 ??
int a = 0, b = 1;
while (true) {
co_yield a; // 變個魔術,產(chǎn)生下一個數(shù)! ?
auto tmp = a;
a = b;
b = tmp + b;
}
}
有了這些強大的新工具,寫并發(fā)代碼簡直就像在玩積木一樣有趣! ?? 再也不用被那些繁瑣的同步問題困擾啦!讓我們一起擁抱這個多線程的新時代吧! ??
性能與調(diào)試提示
嘿,小伙伴們!寫多線程代碼時,最容易掉進的坑就是"線程越多越好"的誤區(qū)啦!?? 這就像開派對一樣,人多不一定熱鬧,可能反而會踩踩踩!
來看個常見的"踩坑"案例:
// ? 不推薦
void process_items(const std::vector<Item>& items) {
std::vector<std::thread> threads;
// 每個任務都開一個新線程,CPU: 我太難了! ??
for (constauto& item : items) {
threads.emplace_back([&item]{ process(item); });
}
}
// ? 推薦
void process_items(const std::vector<Item>& items) {
// 讓CPU告訴我們它能同時處理多少線程 ??
constauto thread_count = std::thread::hardware_concurrency();
ThreadPool pool(thread_count); // 建個溫馨的線程小家庭 ??
// 往線程池丟任務,它自己會安排得明明白白的 ??
for (constauto& item : items) {
pool.enqueue([&item]{ process(item); });
}
}
還有個省心小技巧:如果只是想給個數(shù)字加加減減,用原子操作就夠啦!?? 就像點外賣,一個人點完全程序,比叫一群人一起點要順暢多了:
// 這個計數(shù)器特別乖,不用加鎖也不會亂 ??
std::atomic<int> counter{0};
counter++; // 一個頂一個,穩(wěn)得很!??
記住啦:線程不是越多越好,原子操作不是越多越妙,關鍵是要用對地方!就像調(diào)味料一樣,適量才能讓代碼更美味~ ??
寫并發(fā)代碼就是這樣,與其把時間花在處理復雜的同步問題上,不如好好想想怎么讓架構(gòu)更簡單!畢竟,能用一把鎖解決的問題,干嘛要用兩把呢???