std string与线程安全_C++标准库多线程简介Part1
Part1:線程與互斥量
本篇文章將簡(jiǎn)單的介紹一下C++的標(biāo)準(zhǔn)線程庫(kù),本篇內(nèi)容十分基礎(chǔ),如果你有C++多線程相關(guān)的使用經(jīng)驗(yàn)或者知識(shí),就不必在這篇文章上浪費(fèi)時(shí)間了...
如果你認(rèn)為本篇文章對(duì)你有幫助,請(qǐng)點(diǎn)贊!!!
1.進(jìn)程與線程
在介紹標(biāo)準(zhǔn)庫(kù)多線程之前,需要先介紹一下進(jìn)程與線程的概念與它們之間的差別。
進(jìn)程是被執(zhí)行的應(yīng)用程序(程序即指令(code)與數(shù)據(jù)(data)的集合)的實(shí)例(可以啟動(dòng)多個(gè)相同的應(yīng)用程序,它們是不同的進(jìn)程),同時(shí)也是系統(tǒng)資源分配的最小單位(虛擬地址空間等資源,見下圖),一個(gè)進(jìn)程中可以包含多個(gè)線程。
操作系統(tǒng)會(huì)為每個(gè)進(jìn)程分配一定的虛擬地址空間,該虛擬地址空間由進(jìn)程獨(dú)享線程則是CPU進(jìn)行運(yùn)算調(diào)度的最小單位。線程被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。一個(gè)進(jìn)程中可以包含多個(gè)線程,這些線程可以同時(shí)執(zhí)行不同的任務(wù)(例如一個(gè)線程監(jiān)聽用戶輸入,一個(gè)線程執(zhí)行IO任務(wù),它們是同時(shí)進(jìn)行互相獨(dú)立的)。同一個(gè)進(jìn)程中的多個(gè)線程共享操作系統(tǒng)為該進(jìn)程分配的系統(tǒng)資源(如虛擬地址空間,信號(hào)量等...),但同時(shí)多個(gè)線程又獨(dú)立的擁有各自的調(diào)用棧,寄存器環(huán)境,和線程本地存儲(chǔ)(thread-local storage)。
一個(gè)操作系統(tǒng)中可以有多個(gè)進(jìn)程,這些進(jìn)程可以異步(同時(shí))或同步(順序)執(zhí)行,操作系統(tǒng)為這些進(jìn)程分配獨(dú)立的系統(tǒng)資源。而進(jìn)程中又可以擁有多個(gè)線程(至少一個(gè)),這些線程共享進(jìn)程的系統(tǒng)資源,線程又被稱為輕量級(jí)的進(jìn)程。
2.并發(fā)與并行
并發(fā)與并行對(duì)于初學(xué)者來說是很難區(qū)分的兩種概念。
并發(fā)是指的是在一個(gè)重疊的時(shí)間段內(nèi),有多個(gè)任務(wù)(兩個(gè)以上的task)可以被啟動(dòng),執(zhí)行或者完成,但是這并不意味著,這些任務(wù)必須在這一時(shí)間段內(nèi)的某一時(shí)刻同時(shí)被運(yùn)行。比如在一個(gè)擁有單核CPU上計(jì)算機(jī)上,我們可以同時(shí)運(yùn)行瀏覽器和文檔編輯器程序,卻并不會(huì)感受到任何操作上的延遲。這是因?yàn)椴僮飨到y(tǒng)采用了時(shí)間片輪轉(zhuǎn)算法(Round-Robin,RR),操作系統(tǒng)中的每個(gè)進(jìn)程被分配了一定的時(shí)間段(時(shí)間片),操作系統(tǒng)將CPU分配給某一進(jìn)程讓其在處理器上執(zhí)行一個(gè)時(shí)間片。當(dāng)進(jìn)程占用CPU的時(shí)間超過時(shí)間片的時(shí)間后,將由計(jì)時(shí)器發(fā)起中斷請(qǐng)求,隨后操作系統(tǒng)保存該進(jìn)程的執(zhí)行狀態(tài)并將其掛起,然后將CPU分配給另一個(gè)進(jìn)程執(zhí)行一個(gè)時(shí)間片。由于時(shí)間片劃分的很短,而且進(jìn)程間的切換(進(jìn)程間的切換也被稱為上下文切換Context switch)也很快,所以會(huì)給人一種好像多個(gè)進(jìn)程在同時(shí)執(zhí)行的錯(cuò)覺。(在進(jìn)行context switch的時(shí)候也會(huì)占用一定的時(shí)間,需要保存將被掛起的進(jìn)程的執(zhí)行狀態(tài),還需要把將要執(zhí)行的進(jìn)程的指令與內(nèi)存載入到緩存中,在這期間CPU無法執(zhí)行其他指令,因而過多的context switch會(huì)降低CPU的效率。)
在單核CPU上同時(shí)運(yùn)行進(jìn)程A(紅色)與進(jìn)程B(藍(lán)色),灰色為Context switch所占用的CPU時(shí)間并行是指在同一時(shí)刻有多個(gè)任務(wù)同時(shí)運(yùn)行。比如在一個(gè)雙核的CPU上,在A核心上運(yùn)行瀏覽器進(jìn)程,而在B核心上運(yùn)行文檔編輯器進(jìn)程,在兩個(gè)核心上運(yùn)行的進(jìn)程相互獨(dú)立,同時(shí)運(yùn)行(多進(jìn)程并發(fā))。又比如在一個(gè)游戲進(jìn)程中,可以同時(shí)存在一個(gè)邏輯線程(處理游戲邏輯)和一個(gè)IO線程(處理IO任務(wù)),它們可以同時(shí)運(yùn)行在兩個(gè)不同的CPU核心上(多線程并發(fā))。(并發(fā)的概念是包含并行的,并行是多線程的一種形式,多線程是并發(fā)的一種形式。)
使用多進(jìn)程并發(fā)時(shí),進(jìn)程的創(chuàng)建與銷毀速度都比較慢,而且進(jìn)程間的通信也比較復(fù)雜(需要通過套接字,管道等..),但是操作系統(tǒng)會(huì)在進(jìn)程間提供附加的保護(hù)機(jī)制,這可以我們更容易寫出并發(fā)安全的代碼。而使用多線程并發(fā)時(shí),線程的創(chuàng)建與銷毀速度則要更快,由于同一進(jìn)程中的所有線程共享虛擬地址空間,因此線程間的通信開銷要小得多,但由于缺少線程間的數(shù)據(jù)保護(hù),可能會(huì)出現(xiàn)多個(gè)線程同時(shí)讀寫同一數(shù)據(jù)造成的數(shù)據(jù)不一致現(xiàn)象。
在C++11標(biāo)準(zhǔn)中引入了對(duì)于線程的支持,而本篇文章的主要內(nèi)容就與多線程并發(fā)相關(guān)。
3.C++11中的線程
C++11中thread與thread id的定義如下:
//thread 定義 class thread {class id;// native_handle_type 是連接 thread 類和操作系統(tǒng) SDK API 之間的橋梁。typedef implementation - dependent native_handle_type;// 構(gòu)造與析構(gòu)thread() noexcept;template<class F, class… Args> explicit thread(F&f, Args&&… args);~thread();thread(const thread&) = delete;thread(thread&&) noexcept;thread& operator=(const thread&) = delete;thread& operator=(thread&&) noexcept;//void swap(thread&) noexcept;bool joinable() const noexcept;void join();void detach();//獲取線程idid get_id() const noexcept;// 獲取物理線程數(shù)目static unsigned hardware_concurrency() noexcept;//獲取底層實(shí)現(xiàn)定義的線程句柄 native_handle_type native_handle();//thread id定義class id {id() noexcept;// 可以由==, < 兩個(gè)運(yùn)算衍生出其它大小關(guān)系運(yùn)算。bool operator==(thread::id x, thread::id y) noexcept;bool operator<(thread::id x, thread::id y) noexcept;// !=, <=, >=, >...template<class charT, class traits>basic_ostream<charT, traits>&operator<<(basic_ostream<charT, traits>&out, thread::id id);}; }首先std::thread類的對(duì)象是只能夠被移動(dòng)(move,移動(dòng)構(gòu)造,移動(dòng)賦值),而不能被拷貝(copy,拷貝構(gòu)造,拷貝賦值)的。其次thread類存在一個(gè)無參的默認(rèn)構(gòu)造函數(shù),與一個(gè)接受可調(diào)用對(duì)象與可調(diào)用對(duì)象參數(shù)的構(gòu)造函數(shù)。thread類內(nèi)也定義了一個(gè)id類,id類可以表示線程在操作系統(tǒng)內(nèi)的唯一標(biāo)志符,它重載了多個(gè)比較運(yùn)算符還有輸出運(yùn)算符。id類也可以表示線程運(yùn)行狀態(tài),它的默認(rèn)值(thread::id(),構(gòu)造函數(shù))不表示任何執(zhí)行中的線程。如果一個(gè)thread類的實(shí)例,其get_id方法返回的id與id類的默認(rèn)值相等,則該線程實(shí)例處于一下狀態(tài)之一:
- 尚未指定運(yùn)行的任務(wù)
- 線程運(yùn)行完畢
- 線程已經(jīng)被轉(zhuǎn)移 (move) 到另外一個(gè)線程類實(shí)例
- 線程已經(jīng)被分離 (detached)
thread類中還定義了nativehandle方法,可以返回對(duì)應(yīng)平臺(tái)的線程句柄(如linux中pthread的pthread_t),在我們需要使用一些原生線程支持而std::thread不支持的功能上,這個(gè)方法會(huì)比較有用(比如設(shè)置線程的優(yōu)先級(jí))。
thread類的hardware_concurrency靜態(tài)方法可以返回當(dāng)前處理器所支持的最大并發(fā)線程數(shù)(比如我現(xiàn)在正在使用的e3-1230v3,hardware_concurrency的返回值為8)。
線程的移動(dòng)操作只是改變了線程實(shí)例的id,線程的swap操作也是通過移動(dòng)操作實(shí)現(xiàn)的。
3.1線程的管理
之前內(nèi)容提到了一個(gè)進(jìn)程中至少存在一個(gè)線程,這個(gè)線程被稱為主線程,我們可以在任意線程中創(chuàng)建線程類的實(shí)例。每個(gè)線程都需要一個(gè)入口函數(shù),當(dāng)入口函數(shù)返回時(shí),線程就會(huì)退出,主線程的入口函數(shù)為main()。
a.線程的啟動(dòng)
線程的創(chuàng)建十分簡(jiǎn)單,我們只需創(chuàng)建一個(gè)線程類的實(shí)例,并為它傳入一個(gè)可調(diào)用對(duì)象,就可以啟動(dòng)一個(gè)線程了:
void do_work() {std::cout << "work done" << std::endl; }void test() {std::thread worker(do_work);worker.detach(); }這里的可調(diào)用對(duì)象可以是lambda表達(dá)式,std::function,也可以是重載了調(diào)用運(yùn)算符的類,或者成員函數(shù)或普通函數(shù):
class Work { public:void operator()(){std::cout << "callable object" << std::endl;} };void test() {std::thread worker0([]() {std::cout << "lambda call" << std::endl;});worker0.detach();std::thread worker1(Work{});worker1.detach(); }也可以在線程的構(gòu)造函數(shù)中傳入可調(diào)用對(duì)象的參數(shù),此時(shí)線程構(gòu)造函數(shù)的第一個(gè)參數(shù)為可調(diào)用對(duì)象,此后的參數(shù)為可調(diào)用對(duì)象的參數(shù):
class Work { public:void operator()(int id){std::cout << "work id:" << id << std::endl;} };void test() {std::thread worker(Work{}, 0);worker.join(); }如果傳入的可調(diào)用對(duì)象是某個(gè)類的成員函數(shù),則線程構(gòu)造函數(shù)的第一個(gè)參數(shù)為該類型的成員函數(shù)指針,第二個(gè)參數(shù)為指向該類型的實(shí)例的指針,其后為成員函數(shù)的參數(shù):
class Sampler { public:void sample(int random){std::cout << "sample with:" << random << std::endl;} };void test() {Sampler obj;std::thread worker(&Sampler::sample, &obj, 0);worker.join(); }在向線程中傳遞參數(shù)時(shí)需要注意的一點(diǎn)是:默認(rèn)情況下會(huì)將傳遞的參數(shù)拷貝到線程的獨(dú)立內(nèi)存中,即使傳入?yún)?shù)的類型為引用,但是可以使用std::ref將參數(shù)傳遞的方式更改為引用。
void test() {int work_id = 1;std::thread worker([](int &id) {std::cout << "do work:" << id << std::endl;}, std::ref(work_id));work_id = 2;worker.join(); }b.等待線程完成或分離線程
在啟動(dòng)一個(gè)線程后,必須在線程相關(guān)聯(lián)的std::thread對(duì)象銷毀之前,決定以何種方式等待線程結(jié)束(等待線程執(zhí)行結(jié)束(join)還是讓其自主運(yùn)行(detach))。如果在std::thread對(duì)象銷毀前,還沒有作出決定那么在std::thread對(duì)象的析構(gòu)函數(shù)中就會(huì)觸發(fā)std::terminate導(dǎo)致進(jìn)程終止,如下所示:
void test() {{std::thread worker([]() {std::cout << "do work:" << std::endl;});//錯(cuò)誤,未調(diào)用線程的join或者detach函數(shù),會(huì)導(dǎo)致進(jìn)程被終止} }即使在有異常的情況下也必須保證線程能夠被正確的被detach或join:
void test() {std::thread worker(do_work);try{error_fun();}catch (const std::exception&){worker.join();throw;}worker.join(); }也可以使用RAII(資源獲取即初始化,Resource Acquisition Is Initialization)的方式保證線程可以被正確的join:
class ThreadGuard {std::thread &m_thread; public:explicit ThreadGuard(std::thread &t) : m_thread(t) {}~ThreadGuard(){if (m_thread.joinable()){m_thread.join();}}ThreadGuard(ThreadGuard const&) = delete;ThreadGuard &operator=(ThreadGuard const&) = delete; };void test() {{std::thread worker(do_work);ThreadGuard guard(worker);} }當(dāng)在某一線程調(diào)用另一個(gè)線程對(duì)象的join方法時(shí),調(diào)用join的線程就會(huì)被阻塞,直到被調(diào)用的線程執(zhí)行完畢,調(diào)用join的線程才能繼續(xù)執(zhí)行,如下所示,若在主線程調(diào)用test函數(shù),主線程在調(diào)用worker線程的join方法后,會(huì)一直等待worker線程執(zhí)行完畢后才會(huì)繼續(xù)執(zhí)行輸出語(yǔ)句:
void test() {std::thread worker(do_work);worker.join();std::cout << "test done" << std::endl; }如果不想等待線程運(yùn)行結(jié)束(比如一個(gè)在后臺(tái)進(jìn)行垃圾回收的線程),那么就可以調(diào)用detach使被調(diào)用的線程在后臺(tái)自主運(yùn)行,而調(diào)用detach的線程則不會(huì)等待被調(diào)用線程執(zhí)行結(jié)束,會(huì)直接繼續(xù)執(zhí)行。如下所示,執(zhí)行test函數(shù)的線程在調(diào)用worker線程的detach方法后繼續(xù)執(zhí)行,輸出"test exit",而worker線程會(huì)休眠2秒后才會(huì)輸出"awakening",因此"test exit"會(huì)在"awakening"之前輸出:
void sleep() {using namespace std::chrono_literals;std::this_thread::sleep_for(2s);std::cout << "awakening" << std::endl; }void test() {std::thread worker(sleep);worker.detach();std::cout << "test exit" << std::endl; }使用detach時(shí)一定要注意,如果被調(diào)用detach的線程使用了調(diào)用detach線程的局部變量,那么在局部變量生命周期結(jié)束后,若被調(diào)用detach的線程還試圖訪問該局部變量時(shí),就會(huì)出現(xiàn)錯(cuò)誤:
void test() {size_t length = 10;int *value = new int [length];for (size_t i = 0; i < length; i++){value[i] = i;}std::thread worker([&]() {using namespace std::chrono_literals;std::this_thread::sleep_for(5s);for (size_t i = 0; i < length; i++){//會(huì)出現(xiàn)懸空指針std::cout << value[i] << std::endl;}});worker.detach();//局部變量已經(jīng)被釋放delete[] value;std::cout << "test exit" << std::endl; }對(duì)于一個(gè)std::thread對(duì)象,只能對(duì)其調(diào)用一次join或者detach,被調(diào)用join后就無法再次調(diào)用join或者detach,同樣被調(diào)用detach后也無法再次被調(diào)用join或者detach。可以使用std::thread的joinable方法判斷std::thread對(duì)象是否時(shí)可以被join的,對(duì)一個(gè)std::thread對(duì)象在如下幾種情況下joinable方法會(huì)返回false:
- 空線程(在構(gòu)造沒有附加任何運(yùn)行任務(wù))
- 已經(jīng)被調(diào)用join方法的線程
- 已經(jīng)被調(diào)用detach方法的線程
- 已經(jīng)被move的線程
c.線程所有權(quán)的轉(zhuǎn)移
之前提到std::thread對(duì)象是只可以被move,而不能被copy的。可以通過move,轉(zhuǎn)移線程的所有權(quán):
void test() {std::thread thread0(task);//顯式調(diào)用move方法,轉(zhuǎn)移線程所有權(quán)std::thread thread1 = std::move(thread0);std::thread thread2;//對(duì)于臨時(shí)對(duì)象,隱式地調(diào)用move,轉(zhuǎn)移線程所有權(quán)thread2 = std::thread(task);thread1.join();thread2.join(); }被move后的std::thread對(duì)象將不再代表執(zhí)行線程,也無法再被join或者detach。
借助與move操作,我們可以在函數(shù)間,或者容器中轉(zhuǎn)移線程的所有權(quán):
void test() {std::vector<std::thread> workers;for (size_t i = 0; i < 4; i++){//被創(chuàng)建出的線程的所有權(quán)被轉(zhuǎn)移到vector容器中workers.push_back(std::thread(task));}for (auto &t : workers){t.join();} }d.線程的調(diào)度
標(biāo)準(zhǔn)庫(kù)中出了std::thread和id定義外,還有定義了一個(gè)std::this_thread命名空間:
namespace this_thead {thread::id get_id();void yield();template<class Clock, class Duration>void sleep_until(const chrono::time_point<Clock, Duration>& abs_time);template<class Rep, class Period>void sleep_for(const chromo::duration<Rep, Period>& rel_time); }通過getid方法可以獲得當(dāng)前線程的id,而yield,sleep_until和sleep_for方法則可以用于線程的調(diào)度。
調(diào)用yield方法會(huì)使操作系統(tǒng)重新調(diào)度當(dāng)前線程,并允許其他線程運(yùn)行一段時(shí)間。yield函數(shù)的準(zhǔn)確行為依賴于具體實(shí)現(xiàn),特別是使用中的 OS 調(diào)度器機(jī)制和系統(tǒng)狀態(tài)。例如,先進(jìn)先出實(shí)時(shí)調(diào)度器( Linux 的SCHED_FIFO)將懸掛當(dāng)前線程并將它放到準(zhǔn)備運(yùn)行的同優(yōu)先級(jí)線程的隊(duì)列尾(而若無其他線程在同優(yōu)先級(jí),則yield無效果)。
sleep_for則是將當(dāng)前線程阻塞一定時(shí)間段后喚醒,而sleep_until則是阻塞當(dāng)前線程直至某一時(shí)間點(diǎn)后將當(dāng)前線程喚醒:
void test() {std::thread thread_a([]() {using namespace std::chrono_literals;//線程a將被阻塞2sstd::this_thread::sleep_for(2s);});using namespace std::chrono_literals;auto time_point = std::chrono::steady_clock::now() + 10s;std::thread thread_b([=]() {//線程b將被阻塞,并在10s后被喚醒std::this_thread::sleep_until(time_point);});thread_a.join();thread_b.join(); }4.C++11中的互斥量與鎖管理
4.1數(shù)據(jù)競(jìng)爭(zhēng)(Data race)
同一進(jìn)程中的線程共享虛擬地址空間,這一特性為我們帶來便利的同時(shí),也會(huì)產(chǎn)生一些麻煩,特別是在多個(gè)線程共享數(shù)據(jù)時(shí)。如果多個(gè)線程以只讀的方式共享數(shù)據(jù),那么和單線程訪問數(shù)據(jù)的情況沒有什么不同,多個(gè)線程訪問到的數(shù)據(jù)都是一致的。但是如果在多個(gè)線程同時(shí)讀寫共享數(shù)據(jù)時(shí),共享數(shù)據(jù)的一致性就會(huì)被破壞,這種情況也被稱為data race。
在下面的例子中線程a和b共享person變量,a線程對(duì)person數(shù)據(jù)進(jìn)行修改,同時(shí)b線程讀取并輸出person數(shù)據(jù)。由于兩個(gè)線程是同時(shí)運(yùn)行的,所以可能出現(xiàn)a線程剛剛把person數(shù)據(jù)的age成員修改為Dio Brando的age,而同時(shí)b線程剛剛輸出了Jonathan Joestar的名字,正準(zhǔn)備讀取Jonathan Joestar的age時(shí)卻讀到了Dio Brando的age,這里就出現(xiàn)了數(shù)據(jù)的不一致,線程b讀取到的person數(shù)據(jù)既不是Jonathan Joestar的也不是Dio Brando的(或者說一半是Jonathan Joestar的,另一半是Dio Brando的),這種情況就是data race,是我們必須要盡量避免的。下面的程序可能輸出為Jonathan Joestar, 121, 0。
struct Person {std::string m_name;int m_age;int m_gender; };void test() {Person person{ "Jonathan Joestar", 21, 0};std::thread thread_a([&](){using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);person.m_name = "Dio Brando";person.m_age = 121;person.m_gender = 0;});std::thread thread_b([&]() {using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);std::cout << person.m_name << ", " << person.m_age << ", " << person.m_gender;});thread_b.join();thread_a.join(); }4.2使用互斥量保護(hù)共享數(shù)據(jù)
為了保持共享數(shù)據(jù)的一致性,我們可以采用c++標(biāo)準(zhǔn)庫(kù)提供的互斥量(std::mutex, std::recursive_mutex等..)對(duì)共享數(shù)據(jù)進(jìn)行保護(hù)。(一般來說互斥量同一時(shí)刻只能被一個(gè)線程鎖定,在互斥量已經(jīng)被鎖定的情況下,其他線程嘗試鎖定互斥量就會(huì)被阻塞。不過也有一些特殊的互斥量可以同時(shí)被多個(gè)線程鎖定。在之后的內(nèi)容中鎖與互斥量為同義詞)
C++標(biāo)準(zhǔn)庫(kù)中提供的互斥量一般都有定義lock,unlock,trylock三個(gè)方法。這里以std::mutex為例做說明。
- 使用std::mutex的lock方法可以在調(diào)用lock的線程上鎖住互斥量,若互斥量已被其他線程上鎖,則當(dāng)前調(diào)用lock的線程將被阻塞,直其他占有互斥量的線程解鎖互斥量使得當(dāng)前線程獲得互斥量。對(duì)std::mutex來說,在已經(jīng)占有互斥量的線程上調(diào)用lock方法是未定義行為。
- std::mutex的unlock方法可以解鎖當(dāng)前線程占有的互斥量,若在未占有互斥量的線程上調(diào)用unlock則為未定義行為。
- std::mutex的trylock方法可以嘗試鎖定互斥量,若成功鎖定互斥量則返回true,否則返回false。在已經(jīng)占有互斥量的線程上調(diào)用trylock為未定義行為。在互斥量未被任何線程鎖定的情況下,此函數(shù)也可能會(huì)返回false。
在調(diào)用std::mutex的lock方法鎖定互斥量后一定要記得在不需要占有互斥量的時(shí)候調(diào)用unlock解鎖互斥量,否則其他任何想要獲取鎖的線程都會(huì)被阻塞,此時(shí)多線程就可能會(huì)退化成為單線程。占有 std::mutex的線程在std::mutex對(duì)象銷毀前未調(diào)用其unlock方法則為未定義行為,且std::mutex對(duì)象不可復(fù)制也不可移動(dòng)。
4.1中提到的例子可以使用std::mutex來保證共享數(shù)據(jù)的一致性:
void test() {Person person{ "Jonathan Joestar", 21, 0};std::mutex mutex;std::thread thread_a([&](){using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);//鎖住互斥量mutex.lock();person.m_name = "Dio Brando";person.m_age = 121;person.m_gender = 0;//解鎖互斥量mutex.unlock();});std::thread thread_b([&]() {using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);//鎖住互斥量mutex.lock();std::cout << person.m_name << ", " << person.m_age << ", " << person.m_gender;//解鎖互斥量mutex.unlock();});thread_b.join();thread_a.join(); }上面的代碼在使用鎖的情況下,只存在兩種情況:
- 線程a獲取鎖,修改數(shù)據(jù),修改完畢后解鎖互斥量,若線程b在此期間調(diào)用互斥量的lock方法獲取鎖則會(huì)被阻塞,直到線程a解鎖互斥量,線程b讀取到的數(shù)據(jù)為修改后的數(shù)據(jù)。
- 線程b獲取鎖,讀取到未修改的數(shù)據(jù),輸出完畢后解鎖互斥量,若線程a在此期間調(diào)用互斥量的lock方法獲取鎖則會(huì)被阻塞,直到線程b解鎖互斥量,線程a讀才能鎖定互斥量并修改數(shù)據(jù)。
此時(shí)共享數(shù)據(jù)的一致性得到了保證,線程b讀取到的數(shù)據(jù)要么是未修改的數(shù)據(jù),要么是修改后的數(shù)據(jù),不會(huì)讀取到只修改了一部分的數(shù)據(jù)。
4.3死鎖
互斥量雖然可以用來保護(hù)共享數(shù)據(jù),但是也并非完美。
假設(shè)現(xiàn)在有兩個(gè)線程a和b,兩個(gè)互斥量M和N。線程a會(huì)先鎖住互斥量M隨后再鎖住互斥量N,而線程b則會(huì)先鎖住互斥量N然后再鎖住互斥量M:
void test() {std::mutex M;std::mutex N;std::thread thread_a([&](){//鎖住互斥量M.lock();N.lock();change("thread a");//解鎖互斥量M.unlock();N.lock();});std::thread thread_b([&]() {//鎖住互斥量N.lock();M.lock();change("thread b");//解鎖互斥量N.unlock();M.lock();});thread_b.join();thread_a.join(); }可能會(huì)出現(xiàn)線程a和線程b在同一時(shí)刻分別鎖住了互斥量M和N,隨后a想要鎖住互斥量N時(shí)發(fā)現(xiàn)互斥量N已被線程b上鎖,于是線程a被阻塞,而線程b想要鎖住互斥量M時(shí)發(fā)現(xiàn)互斥量M已經(jīng)被線程a上鎖,于是線程b也被阻塞。兩個(gè)線程都想要鎖住對(duì)方占有的互斥量,于是兩個(gè)線程便僵持不下,誰(shuí)也無法繼續(xù)運(yùn)行,這種狀況就被稱為死鎖。
避免死鎖最簡(jiǎn)單的方法就是在任何時(shí)候都保持以固定順序上鎖互斥量,在持有鎖的時(shí)候也要避免調(diào)用包含鎖操作的代碼(在持有鎖時(shí),調(diào)用包含鎖操作的代碼,可能會(huì)造成死鎖。)對(duì)于上面的例子按這條原則修改如下:
void test() {std::mutex M;std::mutex N;std::thread thread_a([&](){//以固定順序上鎖互斥量M.lock();N.lock();change("thread a");//解鎖互斥量M.unlock();N.lock();});std::thread thread_b([&]() {//以固定順序上鎖互斥量M.lock();N.lock();change("thread b");//解鎖互斥量M.unlock();N.lock();});thread_b.join();thread_a.join(); }以固定順序上鎖可以保持同一時(shí)刻只有一個(gè)線程可以占有互斥量,而其他線程只有等到占有互斥量的線程解鎖互斥量才能夠占有互斥量。
避免死鎖的另一種方法是使用標(biāo)準(zhǔn)庫(kù)提供的鎖管理工具std::lock(c++11)或std::scopedlock(c++17)。在介紹std::lock之前,需要先介紹一下std::lock_guard和std::unique_lock。
std::lock_guard是標(biāo)準(zhǔn)庫(kù)提供的基于RAII的鎖管理工具。std::lock_guard類提供了兩種構(gòu)造函數(shù):
- 在std::lock_guard類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量作為參數(shù),并對(duì)該互斥量進(jìn)行上鎖操作。
- 在std::lock_guard類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量和std::adopt_lock作為參數(shù),互斥的獲取互斥量的所有權(quán),但并不對(duì)互斥量進(jìn)行上鎖。
在std::lock_guard類對(duì)象析構(gòu)時(shí)回對(duì)其占有的互斥量解鎖,除析構(gòu)和構(gòu)造函數(shù)外std::lock_guard沒有定義其他任何方法。
std::unique_lock則RAII式鎖管理的基礎(chǔ)上提供了更多的靈活性。std::unique_lock提供的lock,unlock,trylock方法與其所管理的互斥量提供的lock,unlock,trylock行為相同。std::unique_lock還提供了移動(dòng)構(gòu)造和移動(dòng)賦值操作(支持移動(dòng)操作意味著我們可以在函數(shù)和容器中轉(zhuǎn)移std::unique_lock的所有權(quán)),std::unique_lock的移動(dòng)構(gòu)造函數(shù)會(huì)以參數(shù)的內(nèi)容初始化當(dāng)前對(duì)象,并解除參數(shù)與其所管理的互斥量之前的關(guān)系。在調(diào)用std::unique_lock的移動(dòng)賦值函數(shù)時(shí),若當(dāng)前對(duì)象有互斥量與其關(guān)聯(lián)且已對(duì)其上鎖,則對(duì)互斥量解鎖并解除關(guān)聯(lián),隨后獲取參數(shù)所管理的互斥量,并解除參數(shù)鎖管理的互斥量與參數(shù)間的關(guān)系。
std::unique_lock的構(gòu)造函數(shù)同std::lock_guard的構(gòu)造函數(shù)一樣也提供了初始化策略:
- 在std::unique_lock類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量作為參數(shù),并對(duì)該互斥量進(jìn)行上鎖操作。
- 在std::unique_lock類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量和std::defer_lock作為參數(shù),則不對(duì)該互斥量進(jìn)行上鎖。
- 在std::unique_lock類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量和std::try_to_lock作為參數(shù),則嘗試對(duì)互斥量上鎖,上鎖失敗時(shí)不會(huì)阻塞線程。
- 在std::unique_lock類的對(duì)象在構(gòu)造時(shí)接受一個(gè)互斥量和std::adopt_lock作為參數(shù),則假定當(dāng)前線程已經(jīng)擁有互斥量的所有權(quán)。
std::unique_lock的owns_lock方法可以檢查std::unique_lock是否有互斥量與其關(guān)聯(lián),且是否已對(duì)互斥量上鎖,若有互斥量與std::unique_lock對(duì)象關(guān)聯(lián),且已經(jīng)被std::unique_lock對(duì)象獲得所有權(quán)則返回true,否則返回false。
下面是std::lock_guard與std::unique_lock的簡(jiǎn)單使用示例:
void test() {Person person{ "Jonathan Joestar", 21, 0 };std::mutex mutex;std::thread thread_a([&]() {using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);//關(guān)聯(lián)到mutex并對(duì)其上鎖std::lock_guard lg(mutex);//等價(jià)代碼//std::unique_lock ul(mutex);person.m_name = "Dio Brando";person.m_age = 121;person.m_gender = 0;//lg(或ul)生命周期結(jié)束后解鎖mutex});std::thread thread_b([&]() {using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);//關(guān)聯(lián)到mutex并對(duì)其上鎖std::lock_guard lg(mutex);//等價(jià)代碼//std::unique_lock ul(mutex);std::cout << person.m_name << ", " << person.m_age << ", " << person.m_gender;//lg(或ul)生命周期結(jié)束后解鎖mutex});thread_b.join();thread_a.join(); }介紹完std::unique_lock與std::lock_guard之后,我們?cè)倩氐绞褂脴?biāo)準(zhǔn)庫(kù)提供的鎖管理工具避免死鎖的內(nèi)容上來。
標(biāo)準(zhǔn)庫(kù)提供的std::lock函數(shù)可以配合std::unique_lock或std::lock_guard來避免死鎖。在C++17中提供了基于RAII的更便于使用的std::scopedlock類也可以用于避免死鎖。
現(xiàn)在假設(shè)每條數(shù)據(jù)包含數(shù)據(jù)項(xiàng)和互斥量,在互換兩條數(shù)據(jù)內(nèi)容時(shí),需要對(duì)兩條數(shù)據(jù)的互斥量都進(jìn)行上鎖。下面的代碼展示了如何在這種情況下使用std::lock或std::scopedlock避免死鎖:
struct Datum {//數(shù)據(jù)項(xiàng)std::string m_name;//互斥量std::mutex m_mutex;Datum(const std::string name) : m_name(name) {} };void swap_data(Datum &lhs, Datum &rhs) {using namespace std::chrono_literals;std::this_thread::sleep_for(10ns);//使用std::lock避免死鎖std::lock(lhs.m_mutex, rhs.m_mutex);std::lock_guard lg0(lhs.m_mutex, std::adopt_lock);std::lock_guard lg1(rhs.m_mutex, std::adopt_lock);//等價(jià)代碼//std::unique_lock ul0(lhs.m_mutex, std::defer_lock);//std::unique_lock ul1(rhs.m_mutex, std::defer_lock);//std::lock(ul0, ul1);//C++17等價(jià)代碼,使用std::scoped_lock避免死鎖//std::scoped_lock sl(lhs.m_mutex, rhs.m_mutex);std::string temp = lhs.m_name;lhs.m_name = rhs.m_name;rhs.m_name = temp; }void test() {Datum leon("Leon"), claire("Claire"), ada("Ada"), sherry("Sherry");std::vector<std::thread> workers;workers.emplace_back(swap_data, std::ref(leon), std::ref(ada));workers.emplace_back(swap_data, std::ref(claire), std::ref(ada));workers.emplace_back(swap_data, std::ref(leon), std::ref(sherry));workers.emplace_back(swap_data, std::ref(sherry), std::ref(claire));workers.emplace_back(swap_data, std::ref(sherry), std::ref(ada));workers.emplace_back(swap_data, std::ref(claire), std::ref(leon));for (auto &t : workers){t.join();}std::cout << "Leon's current name is :" << leon.m_name << std::endl;std::cout << "Claire's current name is :" << claire.m_name << std::endl;std::cout << "Ada's current name is :" << ada.m_name << std::endl;std::cout << "Sherry's current name is :" << sherry.m_name << std::endl; }上面代碼的可能輸出為:
Leon's current name is :Sherry Claire's current name is :Claire Ada's current name is :Leon Sherry's current name is :Ada4.4鎖的粒度
在使用互斥量時(shí)除了死鎖,鎖的粒度也是一個(gè)很值得關(guān)注的問題。鎖的粒度是指一個(gè)互斥量鎖保護(hù)的數(shù)據(jù)量的大小。一個(gè)細(xì)粒度的鎖所保護(hù)的數(shù)據(jù)量較小,而一個(gè)粗粒度的鎖所保護(hù)的數(shù)據(jù)量則較大。由于互斥量在同一時(shí)刻只能被一個(gè)線程鎖定,所以在使用粗粒度鎖的情況下一個(gè)線程會(huì)長(zhǎng)時(shí)間占有互斥量,而其他嘗試鎖定互斥量的線程都會(huì)被長(zhǎng)時(shí)間阻塞,這樣程序整體的效率便會(huì)降低。
最能體現(xiàn)鎖的粒度對(duì)程序效率影響的容器可能是hash map,我們知道一個(gè)hash map由多個(gè)bucket組成。假如現(xiàn)在我們需要一個(gè)可供多個(gè)線程安全讀寫的hash map,有如下兩種實(shí)現(xiàn)方法:
- 使用粗粒度鎖。使用一個(gè)互斥量保護(hù)整個(gè)hash map,這種方法實(shí)現(xiàn)起來簡(jiǎn)單粗暴,而且十分有效,但是同一時(shí)刻只有一個(gè)線程能夠讀寫hash map。
- 使用細(xì)粒度鎖。對(duì)組成hash map的每個(gè)bucket分別使用一個(gè)互斥量進(jìn)行保護(hù),這樣一來每個(gè)互斥量所保護(hù)的數(shù)據(jù)量變少了,也可以支持多個(gè)線程同時(shí)讀寫hash map的不同bucket(此時(shí)讀寫同一個(gè)bucket的不同線程還是會(huì)被阻塞)。
上面兩種實(shí)現(xiàn)方案中,使用細(xì)粒度鎖的hash map顯然具有更高的效率。
除了鎖所保護(hù)的數(shù)據(jù)量大小外,持有鎖的時(shí)間的長(zhǎng)短對(duì)程序的運(yùn)行效率也會(huì)有很大的影響。現(xiàn)在假設(shè)要對(duì)一段數(shù)據(jù)依次進(jìn)行讀取,處理和修改操作。為了保證線程安全,我們首先可以考慮使用std::lock_guard對(duì)這一系列操作進(jìn)行保護(hù):
struct DataBlock {std::string m_name; };std::vector<DataBlock> data; size_t index = 0; std::mutex mutex;DataBlock& get_data() {return data[index++]; }void update_data(DataBlock& old_bl, const DataBlock& new_bl) {old_bl = new_bl; }DataBlock process_data(const DataBlock& block) {return DataBlock{ block.m_name + " processed" }; }void get_and_update_data() {std::scoped_lock sl(mutex);DataBlock& original_data = get_data();DataBlock processed_data = process_data(original_data);update_data(original_data, processed_data); }void test() {data.push_back(DataBlock{ "ada" });data.push_back(DataBlock{ "leon" });data.push_back(DataBlock{ "claire" });data.push_back(DataBlock{ "sherry" });std::vector<std::thread> workers;{workers.emplace_back(get_and_update_data);workers.emplace_back(get_and_update_data);workers.emplace_back(get_and_update_data);workers.emplace_back(get_and_update_data);}for (auto &t : workers){t.join();}for (auto &item : data){std::cout << item.m_name << std::endl;} }但是更好的方案是采用更加靈活的std::unique_lock,在不需要持有鎖的時(shí)候?qū)コ饬窟M(jìn)行解鎖(比如進(jìn)行數(shù)據(jù)處理時(shí)),這樣可以減少線程持有鎖的時(shí)間,讓其他線程在當(dāng)前線程處理數(shù)據(jù)時(shí)也有機(jī)會(huì)讀取或者更新數(shù)據(jù):
void get_and_update_data() {std::unique_lock ul(mutex);DataBlock& original_data = get_data();//在不需要持有鎖時(shí),對(duì)互斥量解鎖ul.unlock();DataBlock processed_data = process_data(original_data);//在需要修改共享數(shù)據(jù)時(shí),嘗試獲取鎖ul.lock();update_data(original_data, processed_data); }4.5 C++標(biāo)準(zhǔn)庫(kù)提供的互斥量
在4.2節(jié)中已經(jīng)介紹了一種C++標(biāo)準(zhǔn)庫(kù)提供的互斥量std::mutex,本節(jié)還會(huì)介紹一些標(biāo)準(zhǔn)庫(kù)所提供的其它的互斥量。
a.std::recursive_mutex
std::recursive_mutex與std::mutex一樣也只定義了lock,trylock,unlock和native_handle(用于返回底層實(shí)現(xiàn)定義的原生句柄)方法。
std::recursive_mutex與std::mutex的不同點(diǎn)在于std::recursive_mutex允許在一個(gè)已經(jīng)鎖定互斥量的線程上多次調(diào)用lock方法(與之對(duì)應(yīng)的在一個(gè)已經(jīng)鎖定std::mutex的線程上,再次調(diào)用std::mutex的lock方法是未定義行為),在已鎖定互斥量的情況下再次調(diào)用lock方法會(huì)增加std::recursive_mutex的所有權(quán)等級(jí)。在調(diào)用std::recursive_mutex的unlock方法時(shí),若lock與unlock調(diào)用次數(shù)匹配時(shí)(即所有權(quán)等級(jí)為1時(shí))會(huì)解鎖互斥量,否則會(huì)減少std::recursive_mutex的所有權(quán)等級(jí)。當(dāng)一個(gè)線程鎖定互斥量時(shí),其他線程若嘗試鎖定互斥量就會(huì)被阻塞。(所有權(quán)的最大層數(shù)是未指定的。若超出此數(shù),則可能拋std::system_error類型異常。std::recursive_mutex的lock與unlock,有點(diǎn)類似與COM中的AddRef和release)
下面時(shí)std::recursive_mutex的使用示例:
std::recursive_mutex rmutex;void test() {std::thread worker{ []() {//鎖定互斥量,rmutex的所有權(quán)等級(jí)為1rmutex.lock();do_something();{//在鎖定互斥量的情況下,再次調(diào)用lock,所有權(quán)等級(jí)增加1(為2)rmutex.lock();do_something_else();//所有權(quán)等級(jí)為2,不等于1,將所有權(quán)登記減少1,此次unlock調(diào)用后線程依然占有互斥量rmutex.unlock();}//所有權(quán)等級(jí)為1,此次unlock調(diào)用后線程解鎖互斥量rmutex.unlock();} };worker.join(); }b.std::shared_mutex
std::shared_mutex是c++17引入的共享互斥量。出了提供lock,trylock,unlock方法以支持互斥的單個(gè)線程獨(dú)鎖(排他)定互斥量外,還提供了lock_shared,try_lock_shared和unlock_shared方法以支持多個(gè)線程同時(shí)占有(共享鎖定)互斥量。
std::shared_mutex的lock方法用于排他性的鎖定互斥量,若當(dāng)前線程已經(jīng)以任何模式(排他或共享)占有互斥量則調(diào)用lock方法為未定義行為。std::shared_mutex的unlock方法可以解鎖互斥量,若互斥量未被當(dāng)前線程占有則調(diào)用unlock方法為未定義行為。
std::shared_mutex的lock_shared方法用于獲取互斥量的共享所有權(quán)。若另一線程以排他性所有權(quán)保有互斥,則到lock_shared的調(diào)用將阻塞執(zhí)行,直到能取得共享所有權(quán)。若在以已任何模式(排他性或共享)占有互斥量的線程調(diào)用lock_shared,則為未定義行為。std::shared_mutex的unlock_shared方法用于將當(dāng)前線程占有的共享互斥所有權(quán)釋放。若當(dāng)前線程未以共享方式獲得互斥量所有權(quán),則unlock_shared調(diào)用為未定義行為。
std::shared_mutex多用于多個(gè)線程共享讀取數(shù)據(jù),而只有一個(gè)線程能夠?qū)懭霐?shù)據(jù)的情況。
c.支持時(shí)限的互斥量
標(biāo)準(zhǔn)庫(kù)除了提供std::mutex,std::recursive_mutex和std::shared_mutex外,還提供了與之分別對(duì)應(yīng)的支持時(shí)限的互斥量std::timed_mutex,std::recursive_timed_mutex和std::shared_timed_mutex。這些支持時(shí)限的互斥量除了支持原有互斥量的全部功能外,還提供了try_lock_for和try_lock_until方法。
try_lock_for為在一段時(shí)間內(nèi)嘗試鎖定互斥量,若超過給定時(shí)間段任未獲得鎖(在此期間調(diào)用try_lock_for的線程一直處于阻塞狀態(tài)),則返回false,若在給定時(shí)間段內(nèi)成功鎖定互斥量則返回true。此方法與try_lock方法類似,可能會(huì)在滿足條件的情況下虛假的返回false。
try_lock_until為在給定時(shí)間點(diǎn)之前嘗試鎖定互斥量,若在給定時(shí)間點(diǎn)之后任未獲得鎖(在此期間調(diào)用try_lock_for的線程一直處于阻塞狀態(tài)),則返回false,若在給定時(shí)間點(diǎn)之前成功鎖定互斥量則返回true。此方法與try_lock方法類似,可能會(huì)在滿足條件的情況下虛假的返回false。
此類支持時(shí)限的互斥量,由于調(diào)度或資源爭(zhēng)議延遲等原因,可能調(diào)用對(duì)應(yīng)方法的線程被阻塞的時(shí)間會(huì)超過給定時(shí)間段或超出給定時(shí)間點(diǎn)。
std::timed_mutex tmutex;void test() {//主線程一直持有鎖std::lock_guard lg(tmutex);std::thread worker([]() {auto start = std::chrono::steady_clock::now();//worker線程try_lock_until會(huì)失敗返回false,worker至少會(huì)被阻塞1stmutex.try_lock_until(start + std::chrono::seconds(1));auto end = std::chrono::steady_clock::now();std::chrono::duration<double> time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end - start);std::cout << "Time Span: " << time_span.count() << std::endl;});worker.join(); }以上代碼的可能輸出為Time Span: 1.00057
4.5 C++標(biāo)準(zhǔn)庫(kù)提供的鎖管理工具
在4.3節(jié)中已經(jīng)介紹了C++標(biāo)準(zhǔn)庫(kù)提供的鎖管理工具std::scoped_lock,std::lock,std::lock_guard和std::unique_lock。std::unique_lock除4.3節(jié)中已經(jīng)介紹的功能外,也支持時(shí)限的try_lock_for和try_lock_until方法,其功能與互斥量提供的try_lock_for和try_lock_until方法功能相同。
這里還會(huì)介紹std::shared_lock與std::call_once。
a.std::shared_lock
std::shared_lock類似與std::unique_lock,但通常是與std::shared_mutex一起使用:
std::shared_mutex smutex;int value = 1;std::vector<int> results;//共享讀 void shared_read(int index) {//多個(gè)線程可以同時(shí)共享地鎖定互斥量std::shared_lock<std::shared_mutex> sl(smutex);results[index] = value; }//互斥寫 void exclusive_write() {//只有一個(gè)線程能夠排他的鎖定互斥量std::unique_lock<std::shared_mutex> ul(smutex);value = value * 2; }void test() {results.resize(4);std::vector<std::thread> writers;std::vector<std::thread> readers;writers.emplace_back([]() {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);exclusive_write();});writers.emplace_back([]() {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);exclusive_write();});readers.push_back(std::thread([](int index) {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);shared_read(index);}, 0));readers.emplace_back([](int index) {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);shared_read(index);}, 1);readers.emplace_back([](int index) {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);shared_read(index);}, 2);readers.emplace_back([](int index) {using namespace std::chrono_literals;std::this_thread::sleep_for(20ns);shared_read(index);}, 3);for (auto &t : writers){t.join();}for (auto &t : readers){t.join();}for (auto &var : results){std::cout << var << std::endl;} }上面代碼的可能輸出為:
2 4 4 4b.std::call_once
std::call_once用于在多線程環(huán)境下只執(zhí)行一次的可調(diào)用對(duì)象。標(biāo)準(zhǔn)庫(kù)還提供了一個(gè)輔助類std::once_flag用于指示是否已經(jīng)調(diào)用可調(diào)用對(duì)象。
若調(diào)用std::call_once時(shí),std::once_flag指示可調(diào)用對(duì)象已被調(diào)用,則立即返回,否則調(diào)用可調(diào)用對(duì)象。若調(diào)用可調(diào)用對(duì)象時(shí)出現(xiàn)異常,則傳播異常給call_once的調(diào)用方,并且不翻轉(zhuǎn)once_flag。若調(diào)用成功,則正常返回并翻轉(zhuǎn)once_flag。
下面為std::call_once的使用示例:
std::once_flag flag;//只調(diào)用一次 void prepare_data() {std::cout << "data prepared" << std::endl; }void process(int index) {std::cout << "process data: " << index << std::endl; }void process_data(int index) {std::call_once(flag, prepare_data);process(index); }void test() {std::vector<std::thread> workers;workers.emplace_back([](int index) {process_data(index);}, 0);workers.emplace_back([](int index) {process_data(index);}, 1);workers.emplace_back([](int index) {process_data(index);}, 2);for (auto &t : workers){t.join();} }引用:
What is the difference between concurrency and parallelism??stackoverflow.comC++ 11 多線程--線程管理?www.cnblogs.com使用 C++11 編寫 Linux 多線程程序?www.ibm.com此外還參考了c++ concurrency in action的第1,2,3章節(jié)。
如果你發(fā)現(xiàn)了本篇文章存在的錯(cuò)誤,請(qǐng)指出,我會(huì)及時(shí)修正。
總結(jié)
以上是生活随笔為你收集整理的std string与线程安全_C++标准库多线程简介Part1的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我的KT库之----数据对象
- 下一篇: c++模板类静态成员变量_一文讲透父子类