c++11多線程常用代碼總結


關於

好記性不如爛筆頭
理解雖然到位,但是時間長了就容易忘。
本文僅總結自己經常忘記的知識點, 詳細解釋多線程某些原理、概念。
抱着復習的態度總結此文。
本文參考: cppreference
歡迎指正

0.RAII機制

  • A、RAII=Resource Acquisition Is Initialization,由c++之父Bjarne Stroustrup提出:使用局部對象來管理資源的技術稱為資源獲取即初始化。
  • B、計算中的資源是有限的,內存套接字......比如,,遞歸就需要注意爆棧的情況。默認棧大小,win:8M, linux:1M, 遞歸爆棧是棧空間被用光了。
  • C、原理:充分的利用了C++語言局部對象自動銷毀的特性來控制資源生命周期

1.lock_guard

  • 1.0 第一個參數為 std::mutex 變量,但是其沒有提供lock的成員函數,因為是在構造函數 lock ,析構函數中 unlock
  • 1.1 可以傳遞2個參數,第二個參數指定為 adopt_lock ,則需要手動 lock
  • 1.2 若傳遞一個參數,則不需要手動 lock
  • 1.3 傳遞2個參數情況用法
void proc1(int a)
{
	mtx.lock();//手動鎖定
	// adopt_lock: 當函數結束,g1將釋放互斥鎖
	lock_guard<mutex> g1(mtx, adopt_lock);
        ......
}
  • 1.4 傳遞 1個參數 情況用法
void proc2(int a)
{
	lock_guard<mutex> g2(mtx);//自動鎖定
	...
}

2.unique_lock

  • 2.1 std::unique_lock用法豐富,支持std::lock_guard()的原有功能
  • 2.2 std::unique_lock可以手動lock與手動unlock
  • 2.3 std::unique_lock的第二個參數可以是adopt_locktry_to_lockdefer_lock:
    類型 意義
    adopt_lock 需要手動lock,其構造函數不會lock,析構函數unlock
    try_to_lock 嘗試去鎖定,得保證鎖處於unlock的狀態,然后嘗試現在能不能獲得鎖;嘗試用mutx的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即返回,不會阻塞在那里,並繼續往下執行;
    defer_lock 初始化一個沒有加鎖的mutex
  • 2.4 defer_lock 用法 注意,下面函數的最后一個lock,沒有與之對應的unlock, 是因為析構函數中會自動解鎖。
std::mutex mtx;

void thread_func()
{
  // 初始化一個不加鎖的mutex
  std::unique_lock<std::mutex> locker(mtx, defer_lock);
  ...
  // 現在是std::unique_lock接管mtx,不能調用mtx.lock()和mtx.unlock()
  locker.lock();
  .....
  locker.unlock();
  ....
  locker.lock();
}
  • 2.5 try_to_lock用法,注意 如果 加鎖成功,函數結束后,locker將自動釋放鎖。還有,使用try_to_lock,如果失敗,線程不會阻塞,將會繼續向下執行。
std::mutex mtx;
void proc2()
{
    //嘗試加鎖一次,如果加鎖成功,會立即返回,不會阻塞在那里,且不會再次嘗試鎖操作。
    unique_lock<mutex> locker(m,try_to_lock);

    // 加鎖成功,則會獲取到互斥鎖的擁有權
    if(locker.owns_lock())
    {
        ; // do sth
    }
    // 加鎖失敗,則不會獲取鎖的擁有權,
    else
    {
        ; // do sth
    }
}
  • 2.6 所有權轉移, 使用std::move轉移std::unique_lock的控制權,一個例子:
std::mutex mtx;
{  
    std::unique_lock<std::mutex> locker(m,defer_lock);
    // 所有權轉移,由to_locker來管理互斥量locker, locker已經失去所有權
    std::unique_lock<std::mutex> to_locker(std::move(locker));
    to_locker.lock();
    ...
    to_locker.unlock();
    ...
    to_locker.lock();
    ...
} 

3.condition_variable

Note :這一章節偏長,自己對條件變量的理解不夠深刻,故此加深理解

  • 3.1 注意std::condition_variable類通常與std::mutex類結合使用,std::condition_variable 通常使用 std::unique_lockstd::mutex 來等待

  • 3.2 作用: 同步線程,管理互斥量,控制線程訪問共享資源的順序

  • 3.3 常用函數

    函數名 解釋
    wait 被調用的時候,使用 std::unique_lock 鎖住當前線程, 當前線程會一直被阻塞,直到另外一個線程在相同的 std::condition_variable 對象上調用了 notify_onenotify_all 函數來喚醒線程
    wait_for 可以執行一個時間段,線程收到喚醒通知或者時間超時之前,該線程都會處於阻塞狀態,如果收到喚醒通知或者時間超時,wait_for返回
    wait_until 與wait_for類似,只是wait_until可以指定一個時間點,在當前線程收到通知或者指定的時間點超時之前,該線程都會處於阻塞狀態。如果超時或者收到喚醒通知,wait_until返回
    notify_one 喚醒某個等待(wait)線程。如果當前沒有等待線程,則該函數什么也不做,如果同時存在多個等待線程,則隨機喚醒一個等待的線程
    notify_all 喚醒所有等待(wait)的線程。如果當前沒有等待線程,則該函數什么也不做,注意 驚群效應

    驚群效應
    當多個線程在等待同一個事件時,當事件發生后所有線程被喚醒,但只有一個線程可以被執行,其他線程又將被阻塞,進而造成計算機系統嚴重的上下文切換

  • 3.4 wait: 睡眠當前線程(阻塞操作)。 其實做了兩步操作: A、睡眠當前線程等待條件發生,B、釋放mutex,這樣,其他線程就可以訪問互斥對象。當收到 notify_one() 或者 notify_all() 信號,當前線程會重新嘗試lock, 如果lock成功,則結束等待,函數wait就會返回,否則,則繼續等待。

  • 3.5 為什么需要與 std::unique_lockstd::mutex 一起使用? 考慮下面情況:有兩個線程A和B。線程A調用wait()但線程A 還沒有進入 等待條件狀態的時候,這時線程B調用函數notity_one()喚醒等待條件的線程。 如果不用mutex鎖的話,線程B的notify_one()就 丟失了 。如果 加鎖,情形:線程B必須等到 mutex 被釋放(也就是 線程A的 wait() 釋放鎖並進入wait狀態 ,此時線程B上鎖) 的時候才能調用 notify_one(), 這樣,notify_one() 就不會丟失

  • 3.6 虛假喚醒(spurious awakenings): 當調用函數 notify_one()notify_all() 喚醒, 處於等待的條件變量會重新進行互斥鎖的競爭。沒有得到互斥鎖的線程就會發生等待轉移(wait morphing),從等待信號量的隊列中轉移到等待互斥鎖的隊列中,一旦獲取到互斥鎖的所有權就會接着向下執行,但是此時其他線程已經執行並重置了執行條件,該線程執行就可並引發未定義的錯誤。

  • 3.7 避免 虛假喚醒,可以用下面的代碼避免:

std::unique_lock<std::mutex> lock(_mutex);

// 避免虛假喚醒
while(!pred)  
{
  cv.wait(lock);
  ......
}
  • 3.8 喚醒的位置。 線程的喚醒都是在 內核,內核與內核的切換 和 內核與用戶空間的切換,這些切換是有代價的。互斥的競爭也在內核中。有 2 種情況: A、先是互斥鎖unlock, 再是喚醒notify_one/notify_all(下文簡稱 先unlock再喚醒); B、先是喚醒notify_one/notify_all,再是互斥鎖unlock(下文簡稱先喚醒再unlock

    情況 結果
    先unlock再喚醒 等待的條件變量所在線程被喚醒后拿到互斥鎖的所有權后立即向下執行
    先喚醒再unlock(Linux首推) 是等待條件變量的所在線程被喚醒, 是線程進入互斥鎖的競爭隊列,等待互斥鎖的unlock(兩次內核切換)
  • 3.9 wait函數

    • 3.9.1 形式
    序號 形式
    1 void wait( std::unique_lockstd::mutex& lock );
    2 template< class Predicate > void wait( std::unique_lockstd::mutex& lock, Predicate pred );
    • 3.9.2 參數
    參數 釋義
    lock 類型為std :: unique_lock 的對象,該對象必須由當前線程鎖定
    pred 條件表達式(斷言),等同於 函數 bool pred(); true:wait的等待結束,false:繼續等待
    • 3.9.3 返回值: 無

    • 3.9.4 用法, 代碼來自 這里, but, 自己做了部分修改

// this is from https://en.cppreference.com/w/cpp/thread/condition_variable/wait
#pragma once
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector>

std::condition_variable cv;
std::mutex cv_m; // This mutex is used for three purposes:
				 // 1) to synchronize accesses to i
				 // 2) to synchronize accesses to std::cerr
				 // 3) for the condition variable cv
int i = 0;

void waits(int index)
{
	// 1. it must be lock before waiting
	std::unique_lock<std::mutex> lk(cv_m);
	std::cerr << "index = " << index << "Waiting... \n";
	// 2. block this thread
	cv.wait(lk, [] {return i == 1; });
	std::cerr << "index = " << index << "...finished waiting. i == 1\n";

}

void signals()
{
	std::this_thread::sleep_for(std::chrono::seconds(1));
	{
		std::lock_guard<std::mutex> lk(cv_m);
		std::cerr << "Notifying...\n";
	}
	cv.notify_all();

	std::this_thread::sleep_for(std::chrono::seconds(1));
	{
		std::lock_guard<std::mutex> lk(cv_m);
		i = 1;
		std::cerr << "Notifying again...\n";
	}
	cv.notify_all();

}

int main(int argc, char *argv[])
{
	vector<std::thread> thread_vec;
	thread_vec.push_back(std::thread(waits, 1));
	thread_vec.push_back(std::thread(waits, 2));
	thread_vec.push_back(std::thread(waits, 3));
	thread_vec.push_back(std::thread(signals));

	std::for_each(thread_vec.begin(), thread_vec.end(), std::mem_fn(&std::thread::join));

	return 0;
}

4.std::mutex

  • 4.1 作用 :保護臨界資源,注意條件變量 不同,條件變量控制的是線程同步。
  • 4.2 配對使用: unlocklock 需要 配對使用。切記
  • 4.3 一個簡單例子:
#pragma once
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector>

// 用作保護臨界資源:count_10
std::mutex mtx;
// 臨界資源
int count_10 = 10;
void thread_func()
{
	while (0 < count_10 )
	{
		// 記得需要手動 unlock
		mtx.lock();
		std::cout << "count = " << count_10-- << std::endl;
		// unlock, 及時釋放,服務其他線程
		mtx.unlock();
		
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
}


int main(int argc, char *argv[])
{
	std::vector<std::thread> thread_vec;
	thread_vec.push_back(std::thread(thread_func));
	thread_vec.push_back(std::thread(thread_func));

	std::for_each(thread_vec.begin(), thread_vec.end(), std::mem_fn(&std::thread::join));

	return 0;
}

5.std::async

  • 5.1 頭文件: #include <future>
  • 5.2 std::async: 這是一個函數模板,用於異步執行函數,函數的返回值是一個std::future的對象,如其名,future:將來, 異步函數的返回值將會在將來的某個時刻返回,既然是將來的某個時刻返回,那么他現在是沒有值的,類似 tensorflow 中的占位符。
  • 5.3 policy之 std::launch::asyncstd::launch::deferred
常量 釋義
std::launch::async 新的執行線程(初始化所有線程局域對象后)立即執行,等同於使用std::thread創建線程,線程立即執行,區別於std::thread的是std::thread創建的線程無法獲取線程函數的返回值
std::launch::deferred 延遲啟動線程,甚至可能不會創建線程執行。通常調用 std::future 的get()/wait_*()啟動線程
  • 5.4 std::shared_future, 與 std::future 名字差不多,功能也相似。如其名,shared,共享、分享之意。這兩者都是 用來提前占位,保存線程所在函數的返回值。區別:

    類型 含義
    std::shared_future std::shared_future對象的get() 可被多次調用
    std::future std::future對象的get()只能調用一次
  • 5.5 std::future_status 查詢延遲啟動線程的狀態,future_status狀態定義如下:

    狀態 含義
    std::future_status::deferred 異步操作還沒開始
    std::future_status::timeout 異步操作超時
    std::future_status::ready 異步操作已經完成

當異步操作完成(std::future_status::ready),即可獲取線程返回結果。一個例子:

......
std::future_status status;
do 
{
  // the thread is to start after 3 seconds
  status = future.wait_for(std::chrono::seconds(3));

  if (status == std::future_status::deferred) 
  {
      std::cout << "deferred\n";
  } 
  else if (status == std::future_status::timeout) 
  {
      std::cout << "timeout\n";
  } 
  else if (status == std::future_status::ready) 
  {
      std::cout << "ready!\n";
  }

} while (status != std::future_status::ready);
......
  • 5.6 std::async的基本用法(完整版), 下面的例子分別使用 get()wait()wait_for()啟動異步線程。
// 1. use get() to start the thread 
std::future<int> f1 = std::async(std::launch::async, []() 
{
	return 3;
});

std::cout << f1.get() << std::endl;



// 2. use wait() to start the thread
std::future<int> f2 = std::async(std::launch::async, []() 
{
	std::cout << 3 << endl;
});

f2.wait(); 



// 3. use wait_for() to start it
std::future<int> future = std::async(std::launch::async, []() 
{
	std::this_thread::sleep_for(std::chrono::seconds(3));
	return 3;
});

std::cout << "waiting...\n";
std::future_status status;
do 
{
	status = future.wait_for(std::chrono::seconds(3));

	if (status == std::future_status::deferred) 
	{
		std::cout << "deferred\n";
	}
	else if (status == std::future_status::timeout)
	{
		std::cout << "timeout\n";
	}
	else if (status == std::future_status::ready) 
	{
		std::cout << "ready!\n";
	}
} while (status != std::future_status::ready);

std::cout << "value = " << future.get();

6.std::atomic

Note: 自己在這方面使用偏偏偏 歡迎指正

  • 6.1 頭文件:#include <atomic>

  • 6.2 原子操作: 線程不會被打斷的代碼執行片段。原子操作不可再分。狀態只有 2 種: 操作完成操作沒有完成。常用於一個變量, 而互斥常用於 臨界資源,一片(段)代碼。

  • 6.3 std::atomic 是一個模板類,且頭文件種提供了常用的基礎數據類型的原子類型: boolintcharlong......

  • 6.4 並發編程常用到 原子操作 等概念。 既然是並發,如何保證線程之間不會沖突? 我是這樣理解的: 加鎖解鎖。線程A對 原子變量(可能不夠准確,個人理解) 操作時,先加鎖,xxx, 再解鎖,線程B只能等待線程A釋放鎖才可以加鎖。 只不過加鎖解鎖的過程是由 原子變量 本身提供的,不需要手動 lock 和 unlock

// 定義一個原子變量
std::atomic<int> _count_apple_10(10);

// 線程A
void thread_a()
{
  // 這行代碼,原子變量將完成:加鎖、讀取、解鎖
  std::cout << "apple count = " << _count_apple_10 << "\n";
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM