C++11 多線程編程 學習總結(上)

基本概念

併發:一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。

進程:可執行程序運行,便創建了一個進程。

線程

  • 就是代碼的執行通路
  • 每個進程都有一個主線程,且唯一,與進程一起產生、結束
  • 多線程,不是越多越好,每個線程需要一個獨立的堆棧空間,線程之間切換需切要保存很多中間的狀態,切換過多降低程序的運行時間。

開啓一個線程

整個進程是否執行完畢,取決於主線程是否執行完畢,即如果主線程執行完畢整個進程也結束,此時如果子線程沒有結束,也會被強行結束。因此如果需要讓子線程一保持正常運行,主線程一般會保持一致運行,但是也有例外(即detach)。

開啓線程

1.需要用到的頭文件: #include <thread>since supported by c++11;

2.線程,就是開闢一個新的通道去執行另一個函數,對象如下:

    1)普通函數

    2)仿函數(functor):類裏面實現operator()

    3)Lambda表達式

3.使用的函數:

    1)join()

    2)detach()

    區別

    join()函數,讓主線程等待子線程執行完成,相當於阻塞,join執行完畢,join()後面的主線程纔會繼續執行。這樣可以有效防止主程序已經停止運行,而子程序沒有運行結束,子程序不得不中斷。

    detach()函數,讓子線程和主線程分開獨自執行,無法控制,該子線程由c++運行時庫進行控制管理,駐留後臺運行,運行結束也是由c++運行時庫進行釋放相關資源(守護線程)。但是當主線程結束,該子線程也會隨即中斷結束。

    注意事項

    1)在使用detach函數時,注意子線程和主線程之間不要使用“引用”方式來傳遞參數,否則某一個先結束,釋放變量,會導致另一個線程出現bug。

    2)join()函數一般置於return語句前面,等待所有的子線程運行結束,主線程再結束。

 4.簡單案例

#include <thread>
#include <iostream>
		
void foo() {
    std::cout << "subthread" << std::endl;
}
		
int main(int argc, char const *argv[]){
    std::thread thread(foo);    // 啓動線程foo,並且已經開始執行
	thread.join();              // 讓主線程等待子線程執行完成,相當於阻塞,join執行完畢,主線程再繼續執行
	std::cout<<"I am main thread.\n";
	return 0;
}

    上面關鍵的兩步:

        std::thread thread(foo);   

        thread.join();             

    首先要創建線程的實例,然後進行阻塞。由於Lambda表達式也可以創建函數,因此也可以用Lambda表達式代替foo,同理functor。

線程傳參

1.detach引發的問題

    由於detach使得主線程和該子線程進行了分離,主線程不再等待該子線程完成就結束,就可能會導致如下問題:

    如果從主線程以引用的方式傳參數給子線程,當主線程運行結束,該參數也被釋放,就會導致子線程在接受參數時接收到的是一個被釋放的內存區域,引發不可預料的問題。

    解決辦法:

a)若傳遞int這種簡單類型參數,建議都是值傳遞,不要用引用。防止節外生枝。

b)如果傳遞類對象,避免隱式類型轉換。全部都在創建線程這一行就構建出臨時對象來,然後在函數參數裏用引用來接;否則系統還會構造一次對象

終極結論:

c)建議不使用detach(),只使用join():這樣就不存在局部變量失效導致線程對內存的非法引用問題。

2.獲取當前線程id

std::this_thread::get_id()

3.傳遞類對象的引用到線程函數,std::ref()函數

4.線程函數參數

  • 普通函數
  • 仿函數(functor)
  • 智能指針作爲參數
  • 類的成員函數指針作爲線程參數

(1)functor需要實現operator() ,調用如下,可見只是需要傳入一個類的實例即可,自動的調用functor.

輸入:
class baz{
public:
	void operator()(int n) {
	    std::cout<<"var address in sub: "<<&n<<"|var value in sub: "<<n<<std::endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
	int n = 0;
};
			
int main(int argc, char const *argv[]){
    baz b;
    int n=10;
    std::thread t6(b,n); // t6 runs baz::operator() on object b 
			       
    std::cout<<"var address in main: "<<&n<<"|var value in main: "<<n<<std::endl;
    t6.join();
    return 0;
}
			
輸出:
var address in main: 0x61fe38|var value in main: 10
var address in sub: 0x286fd38|var value in sub: 10

(2)智能指針作爲函數參數

// 傳入一個智能指針
void smartptr(std::unique_ptr<int> ptr){}
			
std::unique_ptr<int> smptr(new int(100)); //不能detach
std::thread threadPtr(smartptr, std::move(smptr));
threadPtr.join();
		
if (smptr == nullptr) 
    std::cout<<"null\n";

當使用智能指針作爲參數時,只能使用std::move的方式將其傳參。並且傳參結束,主線程中的智能指針就變成nullptr。因此此時不能使用detach,只能使用join().

(3)使用類的成員函數

std::thread threadPtr(&class_name::foo, &class_instance, foo_args);

 mutex

創建和等待多個線程

(a)創建多個線程時,各個線程之間是順序不定的執行,由系統調度。

(b)推薦join的寫法,更容易寫出穩定的程序。

(c)把thread對象放入到容器管理,看起來像個thread對象數組,這對我們一次創建大量的線程並對大量線程進行很方便。

 數據共享問題分析

 (1)只讀的數據:是安全穩定的不需要特別什麼處理手段,直接讀就可以;

(2)有讀有寫:2個線程寫,8個線程讀,如果代碼沒有特別的處理,那程序肯定崩潰;

         最簡單的不崩潰處理,讀的時候不能寫,寫的時候不能讀。2個線程不能同時寫,8個線程不能同時讀;

         寫的動作分10小步;由於任務切換,導致各種詭異事情發生(最可能的詭異事情還是崩潰);

保護共享數據過程

(1)操作共享數據時候,爲了保護共享數據,需要鎖住共享數據。

(2)某個線程A,最先佔用線程時,先lock()操作共享數據的代碼。其他使用共享數據的線程必須等待當前佔用共享數據的線程完程。

(3)A操作完成後,解鎖unlock()。其他線程上。

(4)重複上述步驟操作共享數據。

 實現保護共享數據

1.互斥量:std::mutex

  • std::mutex mtx;

2.unlock()lock()成對使用。

  • mutex理解成一把鎖。當線程佔用共享數據時,先用mtx.lock()函數加鎖。
  • 對於同一個mutex對象,一次只有一處mtx.lock()加鎖成功,誰先搶佔這分數據,就先lock()。只要lock()函數返回表示上鎖成功,否則堵塞直到lock()成功。
  • 當該佔用數據的線程結束時,應該使用unlock(),否則整個程序堵塞無法運行。

注意:需要保護的數據,lock()和unlock()應該設置爲合適的範圍。少了達不到效果,多了耗時。

3.爲防止大家忘記unlock(),引入了一個叫std::lock_guard的類模板:你忘記unlock不要緊,我替你unlock;

std::lock_guard可直接取代lock()和unlock();std::lock_guard構造函數裏執行了mutex::lock(),析構函數裏執行了mutex::unlock();

std::lock_guard<std::mutex> lock_grd(mtx);自動的加鎖/解鎖

 死鎖

(1)概念

當有多個mutex對象,至少兩個,lock()的處理不當容易死鎖。順序一致不會死鎖,相反纔會死鎖。

比如在兩個線程裏,分別如下對兩份代碼,進行lock()/ublock()。A線程裏mtx_1.lock()後,若B線程mtx_2.lock()執行,那麼A線程mtx_2.lock()要想執行成功,必須等待B線程mtx_2.unlock(),可是B中的mtx_1.lock()因爲A線程的mtx_1.lock()已經執行又沒unlock()無法執行,導致無法lock()。因此相互鎖死。

 

線程A 線程B

mtx_1.lock();

mtx_2.lock();

//deal with somethig

mtx_2.unlock();

mtx_1.unlock();

mtx_2.lock();

mtx_1.lock();

//deal with sth

mtx_2.unlock();

mtx_1.unlock();

(2)預防死鎖

  • 不同線程的的不同對象lock()順序一致

  • 使用std::lock()模板

 std::lock()函數模板

互斥量可以實現一次2個及其以上,但是不會存在死鎖問題。如果有一個互斥量沒有鎖住,就會等待,同時把鎖住的互斥量釋放,等待所有的互斥量都鎖住,纔會返回。即,要麼都鎖住,要麼都沒有鎖住。

std::lock(mtx_1, mtx_2);

std::lock()函數模板讓編譯器自己lock(),怎麼才能編譯器自己unlock():配合std::lock_guard<std::mutex>

(a)這裏需要給lock_guard傳入第二個參數,告訴編譯器不再需要lock,因爲前面函數模板std::lock()已經lock一次了,lock_guard是爲了編譯期自己unlock

(b)需要傳入第二個參數:std::adopt_lock

         std::lock(mtx_1, mtx_2);

         std::lock_guard<std::mutex> lockGrd1(mtx_1,std::adopt_lock);

         std::lock_guard<std::mutex> lockGrd2(mtx_2,std::adopt_lock);

unique_lock 類模板

unique_lock 也是爲了能實現編譯器自己lock/unlock

(1)相比較lock_guard,更加靈活,但是效率差,內存佔用多。

(2)unique_lock構造函數的的第二個參數(理解爲標誌位):

(a)std::adopt_lock:使用提前必須已經lock,避免二次lock()。這樣主要是不想自己手動unlock()。這一點和lock_guard()一致。

(b)std::try_to_lock:使用前提不能lock。嘗試去鎖定mutex,但是如果沒有成功,不會阻塞在那,而是會立即返回。確定是否lock成功,可以通過owns_lock()判斷。

例如:

std::unique_lock<std::mutex> uniGrd(mtx, std::try_to_lock);

if(uniGrd.owns_lock()) {

        //拿到了lock()

        //處理一些沒有共享數據的代碼

}

else{

        std::cout<<"thread fail to lock.\n";

}

(c)std::defer_lock:使用前提不能lock。它初始化一個沒有lock過的unique_lock對象。

         unique_lock重要的成員函數配合defer_lock()

         + lock()

         + unlock() 主要是用於處理一些非共享數據的代碼。需要不斷地lock、unlock。以保證效率。

std::unique_lock<std::mutex> uniGrd(mtx_1, std::defer_lock);

//需要處理共享數據的代碼

uniGrd.lock();

 

//處理一些沒有共享數據的代碼

 uniGrd.unlock();

                        

//下面又是一些需要處理共享數據的代碼

uniGrd.lock();

msg.push_back(i);

std::cout<<"generator: "<<i<<std::endl

          + try_lock():嘗試給互斥量加鎖,如果拿到了就返回true,否則返回false.不阻塞。類似上面的try_to_lock。

          + release():返回它所管理的mutex對象指針,並且釋放所有權。即unique_lock對象和mutex對象不再綁定在一起。一旦解除了關係,那麼最終需要自己對這個互斥量對象unlock。

std::unique_lock<std::mutex> uniLock(mtx_1); // 編譯器幫你lock

auto mtxPtr = uniLock.release();  //解除綁定

// do something

mtxPtr->unlock();   //解除綁定後,必須自己去unlock

         注意爲什麼有時需要unlock():因爲你lock鎖住的代碼段越少,執行越快,整個程序運行效率越高。

                    有人也把鎖頭鎖住的代碼多少稱爲鎖的 粒度,粒度一般用粗細來描述;

                            > 鎖住的代碼少,這個粒度叫細,執行效率高;

                            > 鎖住的代碼多,這個粒度叫粗,執行效率低;

                            要學會盡量選擇合適的粒度,是高級程序員的能力和實力的體現;

 (3)unique_lock所有權的傳遞

  • 使用移動語義,std::move()。
  • mutex對象的所有權的傳遞只能移動不能複製,像極了unique_ptr

 

 

 

 

 

 

 

 

 

 

 

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