c++11併發編程入門

簡介


併發能夠充分利用多核心處理器,但並行編程卻面臨着嚴峻的挑戰。

並行編程的一個常見問題是數據同步,即多個語句同時訪問同一資源,當一個線程在寫,而另一個在讀時,就會造成不可預料的後果。

加鎖可以在避免上述問題,但使用鎖本身也帶來了一系列問題,如死鎖、效率低下等。不良的代碼也可能造成cpu空轉等待等資源浪費。

在c++11之前,語言或標準庫都沒有對併發進行任何的支持。但c++11提供了以下支持:

  • 語言核心定義了一個內存模型,保證當更改“被兩個不同線程使用”的兩個object時,它們彼此獨立,並引用thread_local關鍵字。
  • 標準庫支持啓動多線程,包括傳遞實參,返回數值、跨線程邊界傳遞異常、同步化等,使得控制流程和數據訪問同步成爲可能。

標準庫提供了高級接口,可以啓動線程,它是架構在低層接口上的。提供的低層接口包括mutex/atomic。

高級接口async()和future


它們的功能:

  • async()提供一個接口,讓函數或函數對象嘗試在後臺運行,成爲一個獨立線程
  • future允許等待線程結束並獲取返回值(可能是異常)

注意,async()是嘗試讓函數在後臺運行,不提供強制性保證。具體地說:

  • 如果有線程處於可用狀態,它的確會啓動
  • 如果環境不支持多線程,或者當時無線程可用,它會推遲函數的異步執行,直到程序明確要獲取其結果

比如要計算兩個數據的各,這兩個數據是兩個函數的返回值,通常的順序型編程爲:func1() + func2()。

並行處理程序如下:

#include <future>
#include <thread>
#include <iostream>
#include <chrono>
#include <random>
#include <exception>

using namespace std;

int DoSomething(char c)
{
	// 隨機數生成器
	std::default_random_engine dre(c);
	std::uniform_int_distribution<int> id(10, 1000);
	
	for (int i = 0; i < 10; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(id(dre))); // 休眠隨機一段時間
		cout.put(c).flush();
	}
	
	return c;
}

int func1()
{
	return DoSomething('.');
}

int func2()
{
	return DoSomething('+');
}

int main()
{
	cout << "begin..." << endl;
	std::future<int> ret1(std::async(func1)); // 異步方式啓動func1,它會立即以另一線程啓動或者等待
	int ret2 = func2(); // 同步方式調用func2
	int ret = ret1.get() + ret2; // 得到func1的運行結果並求和
	cout << "ret = " << ret << endl;
}

兩個函數會以可視化的方式打印字符,並最終返回該字符的int值。注意以下幾點:

  • std::future<int> ret1(std::async(func1));嘗試異步啓動func1於後臺,並將結果賦值給future對象
  • async會嘗試將所獲得的函數立刻異步啓動於一個分離的線程內,因此func1被啓動了,且不會造成main的停滯
  • future是必要的,原因有2:
    • 程序需要獲取異步線程的返回值
    • 它可以確保異步函數最終被調用,這個是強制性的。程序需要確保異步線程執行完畢。
  • future的get()函數確保異步函數最終被調用,具體地說,以下三種情況之一會發生:
    • 如果func1已經啓動並且執行完畢,會立刻獲得結果
    • 如果func1已經啓動但未執行結束,get會引發停滯,等待函數執行結束後返回結果
    • 如果func1尚未啓動,它會強迫啓動如同一個同步調用,直到運行結束並返回結果
  • 這樣比通常的順序調用節省約func1的執行時間
  • 爲了取得最佳效果,通常程序應使調用async和get之間的間隔儘量大
  • 可以使用參數強迫async不推遲目標函數的執行,如async(std::launch::async, func1),但這可能會拋出異常
  • 也可以強制延遲函數的執行,如async(std::launch::deferred, func1),只有在調用get時,函數都會被執行,適用於緩式求值的情況
  • 以上程序未考慮數據同步和異常處理

低層接口:thread/promise


  1. thread

可以直接使用thread對象啓動線程:

std::thread t(DoSomething);

注意,要麼線程被join,要麼將它detach,否則會導致程序崩潰。

相對高級接口async,它的區別:

  • 不提供發射策略,它會立即啓動一個新線程,若出錯則拋出異常
  • 不能處理線程結果,只能獲取唯一的線程id
  • 若線程內發生異常且未被捕捉,程序會終止
  • 線程必須被join或detach,否則會崩潰
  • 如果線程運行期間,main結束了,那麼所有線程也會被硬性終止
  1. promise

用於在線程之間傳遞參數和處理異常,它與future配對使用。

future使用promise的get_future函數獲取關聯,然後,對於promise設置的值(set_value),future可以獲取(get)。

示例如下:

#include <thread>
#include <future>
#include <iostream>
#include <string>
#include <exception>
#include <functional>
#include <utility>

using namespace std;

void DoSomething(std::promise<string>& p)
{
	try
	{
		char c = cin.get();
		if (c == 'x')
		{
			throw std::runtime_error(string("char ") + c + " read");
		}
		
		string s = string("char ") + c + " processed";
		p.set_value(std::move(s));
	}
	catch(...)
	{
		p.set_exception(std::current_exception());
	}
}

int main()
{
	try
	{
		std::promise<string> p;
		std::thread t(DoSomething, std::ref(p));
		t.detach();
		
		std::future<string> f(p.get_future());
		
		cout << "ret: " << f.get() << endl;
	}
	catch(...)
	{
		cout << "err" << endl;
	}
}

數據同步


數據競險及數據不同步的問題不再重複,c++標準庫提供了多種方法使程序在併發數據訪問方面獲得額外的保證:

  • 使用 future和promise,它們保證原子性和次序,一定是結果返回後才設定狀態,不會同步讀寫
  • 使用mutex/lock,它提供對資源的獨佔權,防止對數據併發讀寫
  • condition variable,使線程間等待條件變量的變更,這樣就保證了次序
  • atomic,原子變量,它保證對數據的訪問是不可分割的

這些方式從上到下,從高級到低級,根據使用場景的不同選擇使用。

關於各類的用法就不展開敘述了,可以參考相關示例。

最後


c++11對併發編程提供的支持遠不止這些,可以在實踐中不斷深入學習。

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