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