C++多線程基礎教程
1 什麼是C++多線程?
線程:線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,進程包含一個或者多個線程。進程可以理解為完成一件事的完整解決方案,而線程可以理解為這個解決方案中的的一個步驟,可能這個解決方案就這隻有一個步驟,可能這個解決方案有多個步驟。
多線程:多線程是實現並發(並行)的手段,並發(並行)即多個線程同時執行,一般而言,多線程就是把執行一件事情的完整步驟拆分為多個子步驟,然後使得這多個步驟同時執行。
C++多線程:(簡單情況下)C++多線程使用多個函數實現各自功能,然後將不同函數生成不同線程,並同時執行這些線程(不同線程可能存在一定程度的執行先後順序,但總體上可以看做同時執行)。
上述概念很容易因表述不準確而造成誤解,這裡沒有深究線程與進程,並發與並行的概念,以上僅為一種便於理解的表述,如果有任何問題還請指正,若有更好的表述,也歡迎留言分享。
2 C++多線程基礎知識
2.1 創建線程
首先要引入頭文件#include
有兩種線程阻塞方法join()與detach(),阻塞線程的目的是調節各線程的先後執行順序,這裡重點講join()方法,不推薦使用detach(),detach()使用不當會發生引用對象失效的錯誤。當線程啟動後,一定要在和線程相關聯的thread對象銷毀前,對線程運用join()或者detach()。
join(), 當前線程暫停, 等待指定的線程執行結束後, 當前線程再繼續。th1.join(),即該語句所在的線程(該語句寫在main()函數裏面,即主線程內部)暫停,等待指定線程(指定線程為th1)執行結束後,主線程再繼續執行。
#include<iostream>
#include<thread>
using namespace std;
void proc(int a)
{
cout << "我是子線程,傳入參數為" << a << endl;
cout << "子線程中顯示子線程id為" << this_thread::get_id()<< endl;
}
int main()
{
cout << "我是主線程" << endl;
int a = 9;
thread th2(proc,a);//第一個參數為函數名,第二個參數為該函數的第一個參數,如果該函數接收多個參數就依次寫在後面。此時線程開始執行。
cout << "主線程中顯示子線程id為" << th2.get_id() << endl;
th2.join();//此時主線程被阻塞直至子線程執行結束。
return 0;
}
2.2 互斥量使用
什麼是互斥量?
多個線程並發執行的時候,可能存在線程1正在操作數據a,同時線程2也在操作數據a的情況,兩個線程的操作可能產生衝突。
此時,規定不管是哪個線程,在操作數據a之前都要向領導申請許可證,操作完成後再向領導歸還許可證,許可證總共只有一個,只有拿到許可證的人才能操作數據a,那麼,這個許可證就是互斥量。互斥量保證了操作數據a這一過程不被打斷。
程序實例化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在合適的地方被解鎖:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//實例化m對象,不要理解為定義變量
void proc1(int a)
{
lock_guard<mutex> g1(m);//用此語句替換了m.lock();lock_guard傳入一個參數時,該參數為互斥量,此時調用了lock_guard的構造函數,申請鎖定m
cout << "proc1函數正在改寫a" << endl;
cout << "原始a為" << a << endl;
cout << "現在a為" << a + 2 << endl;
}//此時不需要寫m.unlock(),g1出了作用域被釋放,自動調用析構函數,於是m被解鎖
void proc2(int a)
{
{
lock_guard<mutex> g2(m);
cout << "proc2函數正在改寫a" << endl;
cout << "原始a為" << a << endl;
cout << "現在a為" << a + 1 << endl;
}//通過使用{}來調整作用域範圍,可使得m在合適的地方被解鎖
cout << "作用域外的內容3" << endl;
cout << "作用域外的內容4" << endl;
cout << "作用域外的內容5" << endl;
}
int main()
{
int a = 0;
thread proc1(proc1, a);
thread proc2(proc2, a);
proc1.join();
proc2.join();
return 0;
}
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()的原有功能。
unique_lock的第二個參數,除了可以是adopt_lock,還可以是try_to_lock與defer_lock;
try_to_lock: 嘗試去鎖定,得保證鎖沒有lock,然後嘗試現在能不能獲得鎖;嘗試用mutx的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即返回,不會阻塞在那裡
defer_lock: 始化了一個沒有加鎖的mutex;
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
unique_lock<mutex> g1(m, defer_lock);//始化了一個沒有加鎖的mutex
cout << "不拉不拉不拉" << endl;
g1.lock();//手動加鎖,注意,不是m.lock();注意,不是m.lock();注意,不是m.lock()
cout << "proc1函數正在改寫a" << endl;
cout << "原始a為" << a << endl;
cout << "現在a為" << a + 2 << endl;
g1.unlock();//臨時解鎖
cout << "不拉不拉不拉" << endl;
g1.lock();
cout << "不拉不拉不拉" << endl;
}//自動解鎖
void proc2(int a)
{
unique_lock<mutex> g2(m,try_to_lock);//嘗試加鎖,但如果沒有鎖定成功,會立即返回,不會阻塞在那裡;
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所有權的轉移
mutex m;
{
unique_lock<mutex> g2(m,defer_lock);
unique_lock<mutex> g3(move(g2));//所有權轉移,此時由g3來管理互斥量m
g3.lock();
g3.unlock();
g3.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對象的成員函數get()時阻塞主線程,等待子線程返回結果。
相當於你去辦政府辦業務(主線程),把資料交給了前台,前台安排了人員去給你辦理(創建子線程),前台給了你一個單據(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);//創建線程並返回;
cout << "正在進行計算" << endl;
cout << "計算結果馬上就準備好,請您耐心等待" << endl;
cout << "計算結果:" << fu.get() << endl;//阻塞主線程,直至子線程return
return 0;
}
實例
前一章內容為了簡單的說明一些函數的用法,所列舉的例子有些牽強,因此在本章列舉了一些多線程常見的實例
生產者消費者問題
/*
生產者消費者問題
*/
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include<Windows.h>
using namespace std;
deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//緩衝區的產品個數
void producer() {
int data1;
while (1) {//通過外層循環,能保證生成用不停止
if(c < 3) {//限流
{
data1 = rand();
unique_lock<mutex> locker(mu);//鎖
q.push_front(data1);
cout << "存了" << data1 << endl;
cond.notify_one(); // 通知取
++c;
}
Sleep(500);
}
}
}
void consumer() {
int data2;//data用來覆蓋存放取的數據
while (1) {
{
unique_lock<mutex> locker(mu);
while(q.empty())
cond.wait(locker); //wati()阻塞前先會解鎖,解鎖後生產者才能獲得鎖來放產品到緩衝區;生產者notify後,將不再阻塞,且自動又獲得了鎖。
data2 = q.back();//取的第一步
q.pop_back();//取的第二步
cout << "取了" << data2<<endl;
--c;
}
Sleep(1500);
}
}
int main() {
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
4 C++多線程高級知識
未完待續
5 延伸拓展
創建類,除了傳遞函數外,還可以使用:Lambda表達式、重載了()運算符的類的實例。
線程與進程
並發與並行:
並發與並行並不是非此即彼的概念
並發:同一時間發生兩件及以上的事情。
線程並不是越多越好,每個線程都需要一個獨立的堆棧空間,線程切換也會耗費時間。
並行:
detach()
未完待續