c++中的多線程:概念、基本用法、鎖以及條件變量

1.基本概念

首先要對併發,進程,線程有基本的概念。

1.1什麼是併發

意思就是兩個任務同時執行。

對於單核CPU:在不考慮Intel超線程技術的情況下,由於只有一個CPU,某一時刻只能執行一個任務,因此只能軟件併發,多任務併發情景下需要進行任務切換,因此這個並不是幀併發,而是假併發。

對於多核CPU:因爲有多個CPU,所以可以同時執行多個任務;因此可以硬件併發,真正同時執行多個任務。

1.2什麼是進程

進程是系統資源分配的最小單位,是應用程序運行的環境。打開Windows的進程管理器,你會發現Chrome瀏覽器、Word等應用都是以進程的形式被管理的。

1.3什麼是線程

線程是任務執行的最小單位,一般是執行某個function。

1.4進程與線程的關係

線程屬於進程,一個進程可以擁有多個線程。每個進程都有一個主線程。

一個進程中的所有線程,是共享資源的,比如全局的變量、對象、指針、引用等等。

舉個例子,在word這個應用進程中,有負責UI的線程,有負責字數統計的線程,所有的線程的功能加起來,就是word這個應用進程的全部功能。當某個線程執行時需要資源時,就從word進程的資源池裏取。

其實單線程也可以跑word,只不過多線程可以最大化的發揮出多核CPU的性能優勢。

1.5多進程併發和多線程併發的區別

一個進程中創建了多個線程,一個進程中的線程共享地址空間,全局變量、指針、引用,都可以在線程間傳遞,使用多線程的開銷遠遠小於多進程。

多進程併發,這些進程之間沒有共享地址空間,無法直接傳遞變量、指針、引用等資源,因此數據同步帶來的開發難度與效率會弱於多線程。

 

2.C++中多線程的創建、啓動與應用

c++ 11 之後有了標準的線程庫:std::thread

基本作用:開啓另一個線程並行執行函數/任務。

 

2.0最基本用法,什麼都不用(join和detach都不用)

效果是起一個線程,執行線程中的函數,直到結束,函數結束時,子線程會自動被銷燬。

線程創建之後就開始執行了,只不過在一般的示例中,main函數中起了線程之後,如果不用join或者detach會報錯,這是因爲main已經結束了,而子線程還沒結束,但是資源已經被釋放掉了,就會報錯。

而一般實際項目中,main會一直跑,比如某個service跑在循環裏,這個時候子線程什麼都不用,也可以一直跑着,並不會報錯。

2.1join等待thread執行完畢:

#include<iostream>
#include<thread>
using namespace std;
 
void func_thread() {
    // sleep 3s
    sleep(3000);
    cout << "in func_thread, sleeped 3s.." << endl;
}
 
int main() {
    cout << "main begin ....." << endl;
    std::thread t1(func_thread);
    // 阻塞當前main主線程,等待子線程執行完畢後,恢復主線程執行
    t1.join();
    cout << "main continue ....." << endl;
 }

常用參數:

get_id()    取得目前的線程 id, 回傳一個 std::thread::id  類型

joinable()    檢查是否可 join

join()   // 阻塞當前線程,等待子線程執行完畢

detach()  // 與該線程分離,一旦該線程執行完後它所分配的資源就會被釋放

native_handle()    取得平臺原生的 native handle.

sleep_for()    // 停止目前線程一段指定的時間

yield()   // 暫時放棄CPU一段時間,讓給其他線程

 

2.2不等待thread執行結束,主線程與子線程分離:detach

void thread_func() {
    while(1){
    cout << " thread_func begin...." << endl;
        sleep(1);
    cout << " thread_func end...." << endl;
    }
}
 
int main() {
    std::thread t1(thread_func);
    cout << "created thread t1....." << endl;;
    t1.detach();
    cout << "detach thread t1....."<< endl;
    while(1){
    cout << "main running" << endl;
    Sleep(2);
    }
    return 0;
}

參數傳入引用:

void myFunc(int n) {
    std::cout << "myFunc n = " << n << endl;
    n += 10;
}
 
void myFunc_ref(int& n) {
    std::cout << "myFunc reference n = " << n << endl;
    n += 10;
}
 
int main() {
 
    int n1 = 5;
    thread t1(myFunc, n1);
    t1.join();
    cout << "main n1 = " << n1 << "\n";
 
    int n2 = 10;
    thread t2(myFunc_ref, std::ref(n2));
    t2.join();
    cout << "main n2 = " << n2 << "\n";
 
    cout << "main thread run over" << endl;
    return 0;
}

2.3 reference to non-static member function must be called

代表std::thread t1(func,...)傳入的func是對象的非靜態成員函數。有可能是因爲不確定線程還在工作時,該對象是否會被free,因此報錯。

2個解決辦法:

  • 方法一:把該函數改爲static函數,這樣該函數屬於類,而不是對象,所以就沒有上述擔憂了
  • 方法二:參考上述class 類成員函數創建線程,如下所示

class 類成員函數創建線程

C++ std::thread 的構建可以傳入class類別中的成員函數,如下範例所示:AA::start 分別建立t1, t2 兩個線程,而 t1傳入 AA::a1 類別函數。

notice :

第一個參數:AA::a1 前面需要添加 &

第二個參數:代表的是那個類對象

後面參數: 按照要求傳入即可

class AA
{
public:
    void a1()
    {
        cout << "a1\n";
    }
 
    void a2(int n) {
        cout << "a2 : " << n << "\n";
    }
 
    void start() {
        thread t1(&AA::a1, this);
        thread t2(&AA::a2, this,10);
 
        t1.join();
        t2.join();
    }
 
private:
 
};

 

3.C++創建多個線程以及數據共享

3.1多線程創建

創建多個線程的步驟與創建第一個線程時一樣,需要注意的時線程被創建後,立刻就開始運行了,因此在不使用join的情況下,很難去規定不同線程的執行順序。

使用join()會讓整個流程更穩定。

3.2數據共享

只讀數據在進行線程間的操作時是安全穩定的,不需要特別的處理手段,直接讀就可以;
有的線程寫有的線程讀,不進行特殊處理,那麼程序肯定會崩潰;

因此解決辦法是: 對於讀和寫操作,在對某個數據進行讀寫操作時,先對數據進行上鎖,其他線程要等待該操作完成後對數據解鎖後纔可以使用

保護共享數據

3.2.1用互斥量(mutex)解決共享數據的保護問題

互斥量(mutex)是個類對象,理解成一把鎖,當多個線程嘗試使用lock()成員函數來加鎖時,只有一個線程可以鎖成功(成功標誌是lock()返回);如果沒鎖成功,那麼線程會卡在lock()這裏不斷進行嘗試去加鎖。

在執行多個線程之間的共享數據的讀寫操作時,在每一個讀寫操作之前進行上鎖lock(),然後在操作完之後進行解鎖unlock(),這樣可以保證同一時刻只有一個線程對數據進行處理。

下面是一個用mutex的例子:

// C++stu_03.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//本例程用於學習創建多線程以及數據共享問題

#include <iostream>
#include <windows.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>

using namespace std;

vector<int>g_vaul = { 1,2,3,4 };//共享數據,只讀

class A {
public:
    //把玩家命令輸入到list中
    void inMsgRecvQueue()
    {
        for (int i=0;i<1000;++i)
        {
            my_mutex.lock();
            cout << "inMsgRecvQueue執行插入一個元素" << i << endl;
            msgRecvQueue.push_back(i);
            my_mutex.unlock();
        }
    }

    //從list中取出玩家命令
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 1000; ++i)
        {
            my_mutex.lock();
            if (!msgRecvQueue.empty())
            {
                //消息不爲空
                int command = msgRecvQueue.front();//返回第一個元素
                msgRecvQueue.pop_front();//移除第一個元素但不返回
                cout << "從消息隊列中取出一個數據"<< command << endl;
                my_mutex.unlock();
            }
            else
            {
                my_mutex.unlock();//進行判斷時要注意每種情況下都要有對應的unlock()
                cout << "outMsgRecvQueue執行,但是消息隊列爲空" << i << endl;
            }
        
        }
    }

private:
    list<int>msgRecvQueue;
    mutex my_mutex;
};

int main()
{
    std::cout << "Hello World!\n";
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//要注意第二個參數是引用才能保證線程中使用的是同一個對象
    thread myInnMsgObj(&A::inMsgRecvQueue, &myobja);    
    myOutMsgObj.join();
    myInnMsgObj.join();



    cout << "I love Arsenal!" << endl;
    return 0;
}

4. 死鎖以及解決方案

4.1死鎖的概念

死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

解釋起來就是:
兩個線程兩把鎖,其中一個線程先lock mutex1,再lock mutex2,另外一個線程先lock mutex2,再lock mutex1

當第一個線程把mutex1給locked住時,第二個線程locked了mutex2,此時兩個線程都要繼續上鎖,但是第一個線程無法上lock mutex2,第二個線程無法上lock mutex1,那麼就會卡在這裏,這時就產生了死鎖。

或者說多個線程進行環路等待,就會出現死鎖:

發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

4.2死鎖的解決辦法

std::lock

功能:

  • 一次鎖住兩個或兩個以上的mutex(至少兩個,多了不限,1個不行)
  • 使用該函數模板不存在因多個線程上鎖順序問題而導致的死鎖情況的發生。
  • 如果mutex中有一個沒鎖住,那麼就會一直等待mutex都鎖住纔會繼續向下執行。

特點:

要麼多個mutex都鎖住,要麼多個mutex都沒鎖住。如果只鎖了一個,另外一個沒有鎖成功,那麼它會立即把已經鎖住的解鎖,避免死鎖的發生。
解鎖時要按常規方式挨個解鎖


例子:

// C++stu_03.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//本例程用於學習創建多線程以及數據共享問題

#include <iostream>
#include <windows.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>

using namespace std;

vector<int>g_vaul = { 1,2,3,4 };//共享數據,只讀

class A {
public:
    //把玩家命令輸入到list中
    void inMsgRecvQueue()
    {
        for (int i=0;i<1000;++i)
        {
            //my_mutex1.lock();//先鎖1,再鎖2
            //my_mutex2.lock();
            std::lock(my_mutex1, my_mutex2);//用來代替上面兩句
            cout << "inMsgRecvQueue執行插入一個元素" << i << endl;
            msgRecvQueue.push_back(i);
            my_mutex1.unlock();
            my_mutex2.unlock();
        }
    }

    //從list中取出玩家命令
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 1000; ++i)
        {
            //my_mutex1.lock();//先鎖1,再鎖2
            //my_mutex2.lock();
            std:lock(my_mutex2, my_mutex1);//用來代替上面兩句
            if (!msgRecvQueue.empty())
            {
                //消息不爲空
                int command = msgRecvQueue.front();//返回第一個元素
                msgRecvQueue.pop_front();//移除第一個元素但不返回
                cout << "從消息隊列中取出一個數據"<< command << endl;
                my_mutex1.unlock();
                my_mutex2.unlock();
            }
            else
            {
                my_mutex1.unlock();
                my_mutex2.unlock();
                cout << "outMsgRecvQueue執行,但是消息隊列爲空" << i << endl;
            }
        
        }
    }

private:
    list<int>msgRecvQueue;
    mutex my_mutex1;
    mutex my_mutex2;
};

int main()
{
    std::cout << "Hello World!\n";
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//要注意第二個參數是引用才能保證線程中使用的是同一個對象
    thread myInnMsgObj(&A::inMsgRecvQueue, &myobja);    
    myOutMsgObj.join();
    myInnMsgObj.join();

    cout << "I love Arsenal!" << endl;
    return 0;
}

 

5.unique_lock

5.1什麼是unique_lock

lock_guard時用來管理mutex的上鎖解鎖的模板類,unique_lock也是用來管理mutex上鎖解鎖的類模板

lock_guard<>可以看出它是一個模板類,它在自身作用域(生命週期)中具有構造時加鎖,析構時解鎖的功能。

unique_lock是一個類模板,比lock_guard更加靈活,效率上低一點,內存佔用大一點。

unique_lock可以直接替換lock_guard,調用unique_lock也不需要手動解鎖,在當前作用域結束時會自動解鎖。

5.2std::defer_lock

作用是初始化一個沒有加鎖的mutex(在使用其他參數時會在創建mutex時直接嘗試加鎖),這樣可以自己控制枷鎖的粒度

5.3unique_lock的成員函數

5.3.1成員函數lock()

對於沒有加鎖的unique_lock對象,可以通過成員函數lock()進行上鎖。

5.3.2成員函數unlock()

對於上鎖的unique_lock對象,可以通過成員函數unlock()進行解鎖。這樣提前解鎖來運行一些不需要共享數據的代碼,這使得我們的代碼設計更加靈活

粒度一般用粗細來描述

鎖住的代碼越少,這個鎖的粒度就細,執行效率就越高
鎖住的代碼越多,這個鎖的粒度就粗,執行效率就越低

例子:

std::unique_lock<std::mutex>myunique_lock(my_mutex1,std::defer_lock);//創建一個沒有加鎖的myunique_lock
myunique_lock.lock();//對myunique_lock進行加鎖操作
//處理一些共享數據代碼
myunique_lock.unlock();
//繼續處理一些非共享代碼
//。。。。。。
//處理完之後可以再次上鎖
myunique_lock.lock();//對myunique_lock進行加鎖操作

5.3.3成員函數try_lock()

在不阻塞的情況下進行lock,如果加鎖成功,那麼返回true,如果沒有加鎖成功,那麼返回false。

5.3.4成員函數release()

通過release()會返回它所管理的mutex對象指針,並釋放所有權(也就是說,這個unique_lock和mutex不再有關係)

release()之後要負責把上鎖的mutex解鎖,否則會報錯。

std::unique_lock<std::mutex>myunique_lock(my_mutex1,std::defer_lock);//創建一個沒有加鎖的myunique_lock
myunique_lock.lock();//對myunique_lock進行加鎖操作
//處理一些共享數據代碼
std::mutex *ptr = myunique_lock.release();//釋放myunique_lock並返回my_mutex1的指針
//處理共享數據
ptr->unlock();//手動解鎖,如果不解鎖會卡住導致程序崩潰

6.條件變量condition_variable、wait、notify_one、notify_all

6.1什麼是條件變量

條件變量是允許多個線程相互交流的同步原語。它允許一定量的線程等待(可以定時)另一線程的提醒,然後再繼續。條件變量始終關聯到一個互斥。

std::condition_variable 類是同步原語,能用於阻塞一個線程,或同時阻塞多個線程,直至另一線程修改共享變量(條件)並通知 condition_variable 。

也就是當條件不滿足時,相關線程被一直阻塞,直到某種條件出現,這些線程纔會被喚醒。

然後通過使用my_cond的成員函數wait、notify_one、notify_all來進行條件相關的操作。

6.2如何使用條件變量

6.2.1使用條件

如果一個線程想要修改變量,必須滿足以下條件:

  • 獲得 std::mutex (典型地通過 std::lock_guard )
  • 在保有鎖時進行修改
  • 在 std::condition_variable 上執行 notify_one 或 notify_all (不需要爲通知保有鎖)

即使共享變量是原子性的,它也必須在mutex的保護下被修改,這是爲了能夠將改動正確發佈到正在等待的線程。

任意要等待std::condition_variable的線程,必須滿足以下條件:

  • 獲取std::unique_lock<std::mutex>,這個mutex正是用來保護共享變量(即“條件”)的
  • 執行wait, wait_for或者wait_until. 這些等待動作原子性地釋放mutex,並使得線程的執行暫停
  • 當獲得條件變量的通知,或者超時,或者一個虛假的喚醒,那麼線程就會被喚醒,並且獲得mutex. 然後線程應該檢查條件是否成立,如果是虛假喚醒,就繼續等待。

note:  所謂虛假喚醒,就是因爲某種未知的罕見的原因,線程被從等待狀態喚醒了,但其實共享變量(即條件)並未變爲true。因此此時應繼續等待

std::condition_variable 只可與 std::unique_lock<std::mutex> 一同使用;此限制在一些平臺上允許最大效率。 std::condition_variable_any 提供可與任何基礎可鎖 (BasicLockable) 對象,例如 std::shared_lock 一同使用的條件變量。

6.2.2 成員函數wait

condition_variable 容許 wait 、 wait_for 、 wait_until 、 notify_one 及 notify_all 成員函數的同時調用。
示例:

#include <iostream>
#include <condition_variable>

using namespace std;

mutex wait_mutex;
condition_variable wait_condition_variable;

// 等待線程函數
void wait_thread_func()
{
    unique_lock<mutex> lock(wait_mutex);
    cout << "等待線程(" << this_thread::get_id() << "): 開始等待通知..." << endl;
    wait_condition_variable.wait(lock);
    cout << "等待線程(" << this_thread::get_id() << "): 繼續執行代碼..." << endl;
}

int main()
{
    thread wait_thread(wait_thread_func);

    this_thread::sleep_for(1s); // 等待1秒後進行通知
    cout << "通知線程(" << this_thread::get_id() << "): 開始通知等待線程..." << endl;
    wait_condition_variable.notify_one();
    wait_thread.join();
    cout << "--- main結束 ---" << endl;
}

6.2.3成員函數notify_one和notify_all

notify_one只會喚醒一個線程的wait(),而如果有多個線程需要等待喚醒,那麼需要使用notify_all()來喚醒所有線程中的wait()。

 

 

參考鏈接:

https://www.runoob.com/w3cnote/cpp-std-thread.html

https://blog.csdn.net/u013620306/article/details/128565614

https://blog.csdn.net/milkhoko/article/details/118282922

https://www.cnblogs.com/xiaohaigegede/p/14008121.html

https://blog.csdn.net/qq_39277419/article/details/99544724

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章