C++多執行緒基礎教程

zizbee發表於2020-08-20

1 什麼是C++多執行緒?

執行緒:執行緒是作業系統能夠進行運算排程的最小單位,它被包含在程式之中,程式包含一個或者多個執行緒。程式可以理解為完成一件事的完整解決方案,而執行緒可以理解為這個解決方案中的的一個步驟,可能這個解決方案就這隻有一個步驟,也可能這個解決方案有多個步驟。
多執行緒:多執行緒是實現併發(並行)的手段,併發(並行)即多個執行緒同時執行,一般而言,多執行緒就是把執行一件事情的完整步驟拆分為多個子步驟,然後使得這多個步驟同時執行。
C++多執行緒:(簡單情況下)C++多執行緒使用多個函式實現各自功能,然後將不同函式生成不同執行緒,並同時執行這些執行緒(不同執行緒可能存在一定程度的執行先後順序,但總體上可以看做同時執行)。
上述概念很容易因表述不準確而造成誤解,這裡沒有深究執行緒與程式,併發與並行的概念,以上僅為一種便於理解的表述,如果有任何問題還請指正,若有更好的表述,也歡迎留言分享。

2 C++多執行緒基礎知識

2.1 建立執行緒

首先要引入標頭檔案#include(C++11的標準庫中提供了多執行緒庫),該標頭檔案中定義了thread類,建立一個執行緒即例項化一個該類的物件,例項化物件時候呼叫的建構函式需要傳遞一個引數,該引數就是函式名,thread th1(proc1);如果傳遞進去的函式本身需要傳遞引數,例項化物件時將這些引數按序寫到函式名後面,thread th1(proc1,a,b);只要建立了執行緒物件,執行緒就開始執行。
有兩種執行緒阻塞方法join()與detach(),阻塞執行緒的目的是調節各執行緒的先後執行順序,這裡重點講join()方法,不推薦使用detach(),detach()使用不當會發生引用物件失效的錯誤。當執行緒啟動後,一定要在和執行緒相關聯的thread物件銷燬前,對執行緒運用join()或者detach()。
join(), 當前執行緒暫停, 等待指定的執行緒執行結束後, 當前執行緒再繼續。th1.join(),即該語句所在的執行緒(該語句寫在main()函式裡面,即主執行緒內部)暫停,等待指定執行緒(指定執行緒為th1)執行結束後,主執行緒再繼續執行。
整個過程就相當於你在做某件事情,中途你讓老王幫你辦一個任務(你辦的時候他同時辦)(建立執行緒1),又叫老李幫你辦一件任務(建立執行緒2),現在你的這部分工作做完了,需要用到他們的結果,只需要等待老王和老李處理完(join(),阻塞主執行緒),等他們把任務做完(子執行緒執行結束),你又可以開始你手頭的工作了(主執行緒不再阻塞)。

#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 互斥量使用

什麼是互斥量?
這樣比喻,單位上有一臺印表機(共享資料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在合適的地方被解鎖

#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()

未完待續

相關文章