基本概念
併發:一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。
進程:可執行程序運行,便創建了一個進程。
線程:
- 就是代碼的執行通路
- 每個進程都有一個主線程,且唯一,與進程一起產生、結束。
- 多線程,不是越多越好,每個線程需要一個獨立的堆棧空間,線程之間切換需切要保存很多中間的狀態,切換過多降低程序的運行時間。
開啓一個線程
整個進程是否執行完畢,取決於主線程是否執行完畢,即如果主線程執行完畢整個進程也結束,此時如果子線程沒有結束,也會被強行結束。因此如果需要讓子線程一保持正常運行,主線程一般會保持一致運行,但是也有例外(即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