天天看點

【多線程】C++11進行多線程開發 (std::thread)建立線程警惕作用域線程不能複制給線程傳參以類成員函數為線程函數以容器存放線程對象互斥量條件變量std::call_once

文章目錄

  • 建立線程
    • std::thread 類
    • 使用join()
    • 使用 detach()
  • 警惕作用域
  • 線程不能複制
  • 給線程傳參
    • 傳遞指針
    • 傳遞引用
  • 以類成員函數為線程函數
  • 以容器存放線程對象
  • 互斥量
    • std::mutex
    • std::lock_guard
  • 條件變量
  • call_once

建立線程

C++11 增加了線程以及線程相關的類, 而之前并沒有對并發程式設計提供語言級别的支援

std::thread 類

使用

std::thread

類來建立線程, 我們需要提供的隻是線程函數, 或者線程對象, 同時提供必要的參數

std::thread

表示單個執行的線程, 使用

thread

類首先會構造一個線程對象, 然後開始執行線程函數,

#include <iostream>
#include <thread> //需要包含的頭

using namespace std;

void func(int a, double b)  //有參數, 參數數量不限
{
    cout << a << ' ' << b << endl;
}

void func2() //無參數
{
    cout << "hello!\n";
}

int main() 
{
    thread t1(func, 1, 2); //提供參數
    thread t2(func2);

    //可以使用 lambda表達式
    thread t3([](int a, double b){cout << a << ' ' << b << endl;}, 3, 4);

    cout << t1.get_id()  << "****" << endl;  //可以使用 get_id() 擷取線程 id
    t1.join();
    t2.join();
    t3.join();

    return 0;
}
           

使用join()

我們知道, 上例中如果主線程 (main) 先退出, 那些還未完成任務的線程将得不到執行機會, 因為 main 會在執行完調用 exit(), 然後整個程序就結束了, 那它的"子線程" (我們知道線程是平級的, 這裡隻是, 形象一點) 自然也就 over 了

是以就像上例中, 線程對象調用

join()

函數,

join()

會阻塞目前線程, 直到線程函數執行結束, 如果線程有傳回值, 會被忽略

使用 detach()

對比于

join()

, 我們肯定有不想阻塞目前線程的時候, 這時可以調用

detach()

, 這個函數會分離線程對象和線程函數, 讓線程作為背景線程去執行, 目前線程也不會被阻塞了, 但是分離之後, 也不能再和線程發生聯系了, 例如不能再調用

get_id()

來擷取線程 id 了, 或者調用

join()

都是不行的, 同時也無法控制線程何時結束

#include <thread>
void func() 
{
	//...
}

int main() 
{
	std::thread t(func);
	t.detach();
	// 可以做其他事了, 并不會被阻塞
	return 0;
}
           

程式終止後, 不會等待在背景執行的其餘分離線程, 而是将他們挂起, 并且本地對象被破壞

警惕作用域

std::thread

出了作用域之後就會被析構, 這時如果線程函數還沒有執行完就會發生錯誤, 是以, 要注意保證線程函數的生命周期線上程變量

std::thread

之内

線程不能複制

std::thread

不能複制, 但是可以移動

也就是說, 不能對線程進行複制構造, 複制指派, 但是可以移動構造, 移動指派

#include <iostream>
#include <thread>

void func() 
{
    std::cout << "here is func" << std::endl;
}

int main() 
{
    std::thread t1(func);
    std::thread t2;
    t2 = t1; //error

    t2 = std::move(t1); //right, 将 t1 的線程控制權轉移給 t2
    
    std::cout << t1.get_id() << std::endl;  //error,t1已經失去了線程控制權

    t1 = std::thread(func); //right, 直接構造, 建立的是臨時對象,是以隐式調用move

    t1 = std::move(t2); //error, 不能通過指派一個新值來放棄一個已有線程, 這樣會直接導緻程式崩潰
}
           

std::thread

=

重載了, 調用

operator=

是移動構造函數, 複制被禁用了,

給線程傳參

傳遞指針

#include <iostream>
#include <thread>

void func(int* a){
   //這裡是直接修改指針指向的位址中的值,并不是修改形參指針的指向,是以傳指針可以改變實參的值
    *a += 10;  
}

int main()
{
    int x = 10;
    std::thread t1(func, &x);
    t1.join();
    std::cout << x << std::endl;

    return 0;
}
           

上例代碼, 可以如願改變

x

的值, 但是看下面的代碼, 當我們傳遞引用時, 卻好像并不能如我們所想:

傳遞引用

#include <iostream>
#include <thread>

void func(int& a)
{
    a += 10;
}

int main()
{
    int x = 10;
    std::thread t1(func, x);  //編譯會報錯
    // std::thread t1(func, std::ref(x)); //正确的寫法
    t1.join();
    std::cout << x << std::endl;

    return 0;
}
           

我們想讓 func 函數對 x 進行更新, 但是實際上給線程傳參會以拷貝的形式複制到線程空間, 是以即使是引用, 引用的實際上是新線程堆棧中的臨時值, 為了解決這個問題, 我們需要使用引用包裝器

std::ref()

改成:

std::thread t1(func, std::ref(x));

實際上, 我的編譯器對于

std::thread t1(func, x);

這段代碼直接給出了編譯錯誤,改成

std::thread t1(func, std::ref(x));

後就沒問題了…

以類成員函數為線程函數

因為類内成員涉及 this 指針, 就和所需的線程函數參數不同了

#include <iostream>
#include <thread>

using namespace std;

class A 
{
public:
    void func1()  
    {
        cout << "here is class A`s func 1" << endl;
    }
    static void func2() 
    {
        cout << "here is class A`s func 2" << endl;
    }

    void func3() 
    {
        thread t1(&A::func1, this);	//非靜态成員函數
        thread t2(A::func2);		//靜态成員函數
        
        t1.join();
        t2.join();
    }
};


int main()
{
    A a;
    thread t1(&A::func1, &a);	//非靜态成員函數
    thread t2(A::func2);		//靜态成員函數
    t1.join();
    t2.join();
    a.func3();
}
           
注意的是, 如果我們選擇将成員函數變成靜态的使用, 那我們就 不能使用非靜态的成員變量了, 解決辦法也很簡單, 給靜态成員函數傳遞該對象的

this

指針就好了。
  • 非靜态成員函數需要加上

    &

    符号,并且應該加上類名和

    ::

    作用符,寫在類外就是

    std::thread t1(&A::func1, &a);

    ,寫在類中就需要用

    this

    ,即

    thread t1(&A::func1, this);

    ;
  • 靜态成員函數不需要加上

    &

    ,也沒有

    this

    。因為靜态成員函數屬于類,不屬于執行個體對象。即

    thread t2(A::func2);

可以參考一下我犯過的這個錯誤

以容器存放線程對象

我們可以用容器儲存建立的多個線程對象, 而當我們像其中插入元素時, 建議使用

emplace_bcak()

而不是

push_back()

我們知道

push_back()

會建立一個臨時對象然後拷貝, 當然自從有了移動語意這裡出發都是移動, 如下例:

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

class A 
{
public:
    void func1() 
    {
        cout << "here is class A`s func 1" << endl;
    }

    void func3() 
    {
        tmpThread.push_back(thread(&A::func1, this));		//(1)
        tmpThread.emplace_back(&A::func1, this);	//(2)
    }
    vector<thread> tmpThread;
};
           

比較上例中 (1) (2)兩處, 明顯發現

emplace_back()

push_back()

調用形式更加簡潔, 他會自動推導直接根據你給出的參數初始化臨時對象

emplace_back

不會觸發複制構造和移動構造, 他會直接原地構造一個元素

是以使用

emplace_back

更加簡潔效率也更加高

互斥量

std::mutex

mutex

類是保護共享資料, 避免多線程同時通路的同步原語;

mutex

也不能複制, 他的

operator=

被禁用。

  • lock

    上鎖, 若失敗則

    阻塞

  • try_lock

    嘗試上鎖, 失敗則傳回
  • unlock

    解鎖
使用時注意死鎖

std::lock_guard

通常不直接使用

mutex

,

lock_guard

更加安全, 更加友善。

他簡化了

lock/unlock

的寫法,

lock_guard

在構造時自動鎖定互斥量, 而在退出作用域時會析構自動解鎖, 保證了上鎖解鎖的正确操作, 正是典型的

RAII

機制

#include <thread>
#include <mutex>

std::mutex myLock;
void func() 
{
    {
        std::lock_guard<std::mutex> locker(myLock);   //出作用域自動解鎖
        //do some things...
    }
    myLock.lock();
    myLock.unlock();
}


int main()
{
    std::thread t(func);
    t.join();
}
           

@zhz: 疑問:

std::lock_guard<std::mutex> locker(myLock);

這句話是鎖定

myLock

這個互斥量,避免其他線程擷取這個互斥量嗎? 還是說,隻鎖住這句話之後的代碼塊,避免别的線程通路該代碼塊???

  • 答:是鎖住這個互斥量。因為一旦有一個線程的某段代碼鎖住了這個互斥量,其他線程就擷取不了這個鎖的權限了。使用同一個互斥量在不同的地方鎖住,是因為這幾個地方代碼肯定會通路同一個變量(或者說共享記憶體區域),不然不需要使用鎖。隻有在多線程下才需要使用鎖。哪怕隻有一個地方使用鎖,還是必要的,因為不同線程都在同一片代碼塊進行寫操作時,也需要加鎖防止同時在這個地方寫造成寫資料混亂。
還有一些其他互斥量, 如

std::recursive::mutex

是遞歸型互斥量, 可以讓同一線程重複申請等等, 就不一一介紹了

條件變量

條件變量是C++11 提供的一種用于等待的同步機制, 可以阻塞一到多個線程, 直到收到另一個線程發出的通知或者逾時, 才會喚醒目前阻塞的線程, 條件變量需要和互斥量配合起來使用

  • std::condition_variable

    該條件變量必須配合

    std::unique_lock

    使用
  • std::condition_variable_any

    可以和任何帶 lock, unlock 的 mutex 配合使用. 他更加通用, 更加靈活, 但是效率比前者差一些, 使用時會有一些額外的開銷

這兩者具有相同的成員函數

通知

  • notify_one

    喚醒一個阻塞于該條件變量的線程。 如果有多個等待的線程, 并沒有會優先喚醒誰的說法。即, 沒有喚醒順序, 是随機的.
  • notify_all

    喚醒所有阻塞于該條件變量的線程

等待

  • wait

    讓目前線程阻塞直至條件變量被通知喚醒
  • wait_for

    導緻目前線程阻塞直至通知條件變量、超過指定時間長度
  • 下面示例中,

    wait_for()

    最後一個參數是預制條件,調用

    wait_for

    的時候,首先就會判斷這個條件,
    • 如果這個條件傳回

      false

      ,那麼會繼續等待;
    • 如果在逾時之前,收到了一個

      notify

      ,那麼他會再次執行這個預制條件來進行判斷,逾時的時候也還會再次執行這個條件,這種可以用在處理隊列事件:
    // wait_for的第一個參數是鎖,第二個參數是時間長度,第三個參數是lambda表達式
    cond_var.wait_for(lck, std::chrono::seconds(20), [] {
      std::this_thread::sleep_for(std::chrono::seconds(1));
      return false;
      });
               
    原文連結:https://blog.csdn.net/najiutan/article/details/110817106
  • wait_until

    導緻目前線程阻塞直至通知條件變量、抵達指定時間點
  • wait_for

    需要相對時間(“等待長達10秒”),而

    wait_until

    需要絕對時間(“等到2012年10月30日12:00”)。

    比較時間參數的聲明:

    // wait_for:
    const std::chrono::duration<Rep, Period>& rel_time
    
    // wait_until:
    const std::chrono::time_point<Clock, Duration>& abs_time
               
    關于條件變量的詳細以及

    wait_for

    wait_until

    用法可參考

    因為

    虛假喚醒

    的存在 和 為了

    避免丢失信号量

    (

    避免丢失信号量

    就是在調用

    wait

    的時候, 在其之前發出的喚醒都不會對

    wait

    生效, 而系統

    不會儲存

    這些條件變量, 調用完就丢掉了),

    我們必須使用

    循環

    判斷條件變量,是以我們使用條件變量必須結合

    mutex

    ,并且将判斷條件放入

    while

    循環, 而不是使用

    if

    std::call_once

    <mutex>

    中還提供了

    std::call_once

    函數,保證某個函數即使在多個線程中同時調用時,也隻被調用一次。使用

    std::call_once

    需要同時使用其幫助結構體

    once_flag

    template< class Callable, class... Args >
    void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
               
    • 如果調用

      call_once

      flag

      已經被設定,說明函數

      f

      已經被調用過了,這種情況下

      call_once

      直接傳回;
    • 如果

      flag

      未被設定,則調用

      call_once

      時會直接調用

      std​::​forward<Callable>(f)

      ,并向其傳遞

      std​::​forward<Args>(args)...

      參數。如果此時

      f

      内抛出了異常,則異常會傳遞給

      call_once

      的調用者,并且不會設定

      flag

      ,這樣可以使得後續使用同一标志調用

      call_once

      時能繼續調用

      f

      函數。
    #include <iostream>
    #include <thread>
    #include <mutex>
    
    using namespace std;
    
    once_flag onlyOnce;
    mutex myMutex;
    
    void func() //線程函數
    {
        myMutex.lock();
        cout << "here is func" << endl;
        myMutex.unlock();
    
        call_once(onlyOnce, []{		//僅僅調用一次
            cout << "hello world!" << endl;
        });
    }
    
    int main() 
    {
        thread t1(func);
        thread t2(func);
        thread t3(func);
    
        t1.join();
        t2.join();
        t3.join();
    
        return 0;
    }
               
    這篇部落格算是拖了好幾個月才寫的了, 寫一半還沒了, 以後寫部落格記得好好儲存…

繼續閱讀