C++多线程详细讲解
本文是純轉載,覺得大佬寫的非常好!如有侵權可以刪除
鏈接: link.
C++多線程基礎教程
目錄
1 什么是C++多線程?
2 C++多線程基礎知識
2.1 創建線程
2.2 互斥量使用
lock()與unlock():
lock_guard():
unique_lock:
condition_variable:
2.3 異步線程
async與future:
shared_future
2.4 原子類型automic
實例
生產者消費者問題
4 C++多線程高級知識
4.1 線程池
線程池基礎知識
線程池的實現
5 延伸拓展
最后一次更新日期:2020.08.23
1 什么是C++多線程?
線程:線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,進程包含一個或者多個線程。進程可以理解為完成一件事的完整解決方案,而線程可以理解為這個解決方案中的的一個步驟,可能這個解決方案就這只有一個步驟,也可能這個解決方案有多個步驟。
多線程:多線程是實現并發(并行)的手段,并發(并行)即多個線程同時執行,一般而言,多線程就是把執行一件事情的完整步驟拆分為多個子步驟,然后使得這多個步驟同時執行。
C++多線程:(簡單情況下)C++多線程使用多個函數實現各自功能,然后將不同函數生成不同線程,并同時執行這些線程(不同線程可能存在一定程度的執行先后順序,但總體上可以看做同時執行)。
上述概念很容易因表述不準確而造成誤解,這里沒有深究線程與進程,并發與并行的概念,以上僅為一種便于理解的表述,如果有任何問題還請指正,若有更好的表述,也歡迎留言分享。
2 C++多線程基礎知識
2.1 創建線程
首先要引入頭文件#include(C++11的標準庫中提供了多線程庫),該頭文件中定義了thread類,創建一個線程即實例化一個該類的對象,實例化對象時候調用的構造函數需要傳遞一個參數,該參數就是函數名,thread th1(proc1);如果傳遞進去的函數本身需要傳遞參數,實例化對象時將這些參數按序寫到函數名后面,thread th1(proc1,a,b);只要創建了線程對象(傳遞“函數名/可調用對象”作為參數的情況下),線程就開始執行(std::thread 有一個無參構造函數重載的版本,不會創建底層的線程)。
有兩種線程阻塞方法join()與detach(),阻塞線程的目的是調節各線程的先后執行順序,這里重點講join()方法,不推薦使用detach(),detach()使用不當會發生引用對象失效的錯誤。當線程啟動后,一定要在和線程相關聯的thread對象銷毀前,對線程運用join()或者detach()。
join(), 當前線程暫停, 等待指定的線程執行結束后, 當前線程再繼續。th1.join(),即該語句所在的線程(該語句寫在main()函數里面,即主線程內部)暫停,等待指定線程(指定線程為th1)執行結束后,主線程再繼續執行。
整個過程就相當于你在做某件事情,中途你讓老王幫你辦一個任務(你辦的時候他同時辦)(創建線程1),又叫老李幫你辦一件任務(創建線程2),現在你的這部分工作做完了,需要用到他們的結果,只需要等待老王和老李處理完(join(),阻塞主線程),等他們把任務做完(子線程運行結束),你又可以開始你手頭的工作了(主線程不再阻塞)。
2.2 互斥量使用
什么是互斥量?
這樣比喻,單位上有一臺打印機(共享數據a),你要用打印機(線程1要操作數據a),同事老王也要用打印機(線程2也要操作數據a),但是打印機同一時間只能給一個人用,此時,規定不管是誰,在用打印機之前都要向領導申請許可證(lock),用完后再向領導歸還許可證(unlock),許可證總共只有一個,沒有許可證的人就等著在用打印機的同事用完后才能申請許可證(阻塞,線程1lock互斥量后其他線程就無法lock,只能等線程1unlock后,其他線程才能lock),那么,這個許可證就是互斥量。互斥量保證了使用打印機這一過程不被打斷。
程序實例化mutex對象m,線程調用成員函數m.lock()會發生下面 3 種情況:
(1)如果該互斥量當前未上鎖,則調用線程將該互斥量鎖住,直到調用unlock()之前,該線程一直擁有該鎖。
(2)如果該互斥量當前被鎖住,則調用線程被阻塞,直至該互斥量被解鎖。
互斥量怎么使用?
首先需要#include
lock()與unlock():
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//實例化m對象,不要理解為定義變量 void proc1(int a) {m.lock();cout << "proc1函數正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現在a為" << a + 2 << endl;m.unlock(); }void proc2(int a) {m.lock();cout << "proc2函數正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現在a為" << a + 1 << endl;m.unlock(); } int main() {int a = 0;thread proc1(proc1, a);thread proc2(proc2, a);proc1.join();proc2.join();return 0; }不推薦實直接去調用成員函數lock(),因為如果忘記unlock(),將導致鎖無法釋放,使用lock_guard或者unique_lock能避免忘記解鎖這種問題。
lock_guard():
其原理是:聲明一個局部的lock_guard對象,在其構造函數中進行加鎖,在其析構函數中進行解鎖。最終的結果就是:創建即加鎖,作用域結束自動解鎖。從而使用lock_guard()就可以替代lock()與unlock()。
通過設定作用域,使得lock_guard在合適的地方被析構(在互斥量鎖定到互斥量解鎖之間的代碼叫做臨界區(需要互斥訪問共享資源的那段代碼稱為臨界區),臨界區范圍應該盡可能的小,即lock互斥量后應該盡早unlock),通過使用{}來調整作用域范圍,可使得互斥量m在合適的地方被解鎖:
lock_gurad也可以傳入兩個參數,第一個參數為adopt_lock標識時,表示不再構造函數中不再進行互斥量鎖定,因此此時需要提前手動鎖定。
#include<iostream> #include<thread> #include<mutex> using namespace std; mutex m;//實例化m對象,不要理解為定義變量 void proc1(int a) {m.lock();//手動鎖定lock_guard<mutex> g1(m,adopt_lock);cout << "proc1函數正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現在a為" << a + 2 << endl; }//自動解鎖void proc2(int a) {lock_guard<mutex> g2(m);//自動鎖定cout << "proc2函數正在改寫a" << endl;cout << "原始a為" << a << endl;cout << "現在a為" << a + 1 << endl; }//自動解鎖 int main() {int a = 0;thread proc1(proc1, a);thread proc2(proc2, a);proc1.join();proc2.join();return 0; }unique_lock:
unique_lock類似于lock_guard,只是unique_lock用法更加豐富,同時支持lock_guard()的原有功能。
使用lock_guard后不能手動lock()與手動unlock();使用unique_lock后可以手動lock()與手動unlock();
unique_lock的第二個參數,除了可以是adopt_lock,還可以是try_to_lock與defer_lock;
try_to_lock: 嘗試去鎖定,得保證鎖處于unlock的狀態,然后嘗試現在能不能獲得鎖;嘗試用mutx的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即返回,不會阻塞在那里
defer_lock: 始化了一個沒有加鎖的mutex;
lock_guard unique_lock
手動lock與手動unlock 不支持 支持
參數 支持adopt_lock 支持adopt_lock/try_to_lock/defer_lock
condition_variable:
需要#include<condition_variable>;
wait(locker):在線程被阻塞時,該函數會自動調用 locker.unlock() 釋放鎖,使得其他被阻塞在鎖競爭上的線程得以繼續執行。另外,一旦當前線程獲得通知(通常是另外某個線程調用 notify_* 喚醒了當前線程),wait() 函數此時再自動調用 locker.lock()。
notify_all():隨機喚醒一個等待的線程
notify_once():喚醒所有等待的線程
2.3 異步線程
需要#include
async與future:
async是一個函數模板,用來啟動一個異步任務,它返回一個future類模板對象,future對象起到了占位的作用,剛實例化的future是沒有儲存值的,但在調用future對象的get()成員函數時,主線程會被阻塞直到異步線程執行結束,并把返回結果傳遞給future,即通過FutureObject.get()獲取函數返回值。
相當于你去辦政府辦業務(主線程),把資料交給了前臺,前臺安排了人員去給你辦理(async創建子線程),前臺給了你一個單據(future對象),說你的業務正在給你辦(子線程正在運行),等段時間你再過來憑這個單據取結果。過了段時間,你去前臺取結果,但是結果還沒出來(子線程還沒return),你就在前臺等著(阻塞),直到你拿到結果(get())你才離開(不再阻塞)。
#include <iostream> #include <thread> #include <mutex> #include<future> #include<Windows.h> using namespace std; double t1(const double a, const double b) {double c = a + b;Sleep(3000);//假設t1函數是個復雜的計算過程,需要消耗3秒return c; }int main() {double a = 2.3;double b = 6.7;future<double> fu = async(t1, a, b);//創建異步線程線程,并將線程的執行結果用fu占位;cout << "正在進行計算" << endl;cout << "計算結果馬上就準備好,請您耐心等待" << endl;cout << "計算結果:" << fu.get() << endl;//阻塞主線程,直至異步線程return//cout << "計算結果:" << fu.get() << endl;//取消該語句注釋后運行會報錯,因為future對象的get()方法只能調用一次。return 0; }shared_future
future與shard_future的用途都是為了占位,但是兩者有些許差別。
future的get()成員函數是轉移數據所有權;shared_future的get()成員函數是復制數據。
因此:
future對象的get()只能調用一次;無法實現多個線程等待同一個異步線程,一旦其中一個線程獲取了異步線程的返回值,其他線程就無法再次獲取。
shared_future對象的get()可以調用多次;可以實現多個線程等待同一個異步線程,每個線程都可以獲取異步線程的返回值。
future shared_future
語義 轉移 賦值
可否多次調用 否 可
2.4 原子類型automic
原子操作指“不可分割的操作”;也就是說這種操作狀態要么是完成的,要么是沒完成的。互斥量的加鎖一般是針對一個代碼段,而原子操作針對的一般都是一個變量。
automic是一個模板類,使用該模板類實例化的對象,提供了一些保證原子性的成員函數來實現共享數據的常用操作。
可以這樣理解:
在以前,定義了一個共享的變量(int i=0),多個線程會操作這個變量,那么每次操作這個變量時,都是用lock加鎖,操作完畢使用unlock解鎖,以保證線程之間不會沖突;
現在,實例化了一個類對象(automic I=0)來代替以前的那個變量,每次操作這個對象時,就不用lock與unlock,這個對象自身就具有原子性,以保證線程之間不會沖突。
automic對象提供了常見的原子操作(通過調用成員函數實現對數據的原子操作):
store是原子寫操作,load是原子讀操作。exchange是于兩個數值進行交換的原子操作。
即使使用了automic,也要注意執行的操作是否支持原子性。一般atomic原子操作,針對++,–,+=,-=,&=,|=,^=是支持的。
實例
前一章內容為了簡單的說明一些函數的用法,所列舉的例子有些牽強,因此在本章列舉了一些多線程常見的實例
生產者消費者問題
/*
4 C++多線程高級知識
4.1 線程池
線程池基礎知識
不采用線程池時:
創建線程 -> 由該線程執行任務 -> 任務執行完畢后銷毀線程。即使需要使用到大量線程,每個線程都要按照這個流程來創建、執行與銷毀。
雖然創建與銷毀線程消耗的時間 遠小于 線程執行的時間,但是對于需要頻繁創建大量線程的任務,創建與銷毀線程 所占用的時間與CPU資源也會有很大占比。
為了減少創建與銷毀線程所帶來的時間消耗與資源消耗,因此采用線程池的策略:
程序啟動后,預先創建一定數量的線程放入空閑隊列中,這些線程都是處于阻塞狀態,基本不消耗CPU,只占用較小的內存空間。
接收到任務后,線程池選擇一個空閑線程來執行此任務。
任務執行完畢后,不銷毀線程,線程繼續保持在池中等待下一次的任務。
線程池所解決的問題:
(1) 需要頻繁創建與銷毀大量線程的情況下,減少了創建與銷毀線程帶來的時間開銷和CPU資源占用。(省時省力)
(2) 實時性要求較高的情況下,由于大量線程預先就創建好了,接到任務就能馬上從線程池中調用線程來處理任務,略過了創建線程這一步驟,提高了實時性。(實時)
線程池的實現
待更新。
延伸拓展
創建類,除了傳遞函數外,還可以使用:Lambda表達式、可調用類的實例。
線程與進程:
并發與并行:
并發與并行并不是非此即彼的概念
并發:同一時間發生兩件及以上的事情。
線程并不是越多越好,每個線程都需要一個獨立的堆棧空間,線程切換也會耗費時間。
并行:
detach():
未完待續
總結
以上是生活随笔為你收集整理的C++多线程详细讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CTS 测试
- 下一篇: Telephony基础架构