C++ std::future期望

(本文參照C++在線手冊和《C++併發編程實戰》)

0.瞭解std::future

C++11提供了類模板std::future來訪問異步操作結果。可以使用future來代表特定的一次性事件,線程週期性的在這個future上檢測事件是否發生,當所需的事件已發生future變爲就緒狀態,事件發生之後future也無法復位。標準庫中有兩類future,唯一future(std::future)對象是僅有的一個指向其關聯事件的實例,而多個共享future(std::shared_future)對象可以指向同一共享狀態。對於共享future而言,所有實例對象將同時變爲就緒。最基本的一次性事件是在後臺運行着的計算結果(這書把異步說的這麼複雜)。

(雖然future被用於進程間通信,但是future對象本身並不提供同步訪問,需要使用互斥鎖或其他同步機制來保護訪問。)

操作流程:

  • (通過 std::async、std::packaged_atsk 或 std::promise 創建的)異步操作能提供一個 std::future 對象給該異步操作的創建者。(如:std::future<int> f = std::async(std::launch::async, [](){ return 8; }); )
  • 然後,異步操作的創建者能用各種方法查詢、等待或從 std::future 提取值。若異步操作仍未提供值,則這些方法可能阻塞。(如:f.wait(); 或 f.get(); )
  • 異步操作準備好發送結果給創建者時,它能通過修改鏈接到創建者的 std::future  的共享狀態進行。(如 : std::promise::set_value )

 類接口:

1.配合異步std::async

可以使用std::async異步地執行一個函數,並返回一個std::future,std::future最終最終將持有函數的返回值。std::async的參數傳遞方式類似std::bind,此外還可以傳遞一個指定調用策略的參數。若設置std::launch::async標誌,則函數在新的線程執行;設置std::launch::deferred則函數在調用wait()或get()時執行,該參數默認爲std::launch::async|std::launch::deferred,即由具體實現來選擇。並且,std::async參數中的可調用對象和給可調用對象的參數需要滿足移動構造的要求。

#include <iostream>
#include <string>
#include <future>

struct X
{
	void foo(int, std::string const&){}
	std::string bar(std::string const&) { return std::string("yes"); }
};

X x;
auto f1 = std::async(&X::foo, &x, 1, "hello"); //調用obj->foo(1,"hello"),其中obj是&x
auto f2 = std::async(&X::bar, x, "goodbye"); //調用obj.bar("goodbye"),其中obj是x的副本

struct Y
{
	double operator()(double f) { return f * f; } //仿函數
};

Y y;
auto f3 = std::async(Y(), 123); //調用obj(123),其中obj是從Y()移動構造,可以自定義移動構造
auto f4 = std::async(std::ref(y), 123); //調用y(123)

X baz(X& x) { 
	std::cout << "baz runing" << std::endl;
	return x; 
}
auto f5 = std::async(baz, std::ref(x));//調用baz(x)

auto f6 = std::async(std::launch::async, Y(), 123);//在新的線程中運行
auto f7 = std::async(std::launch::deferred, baz,std::ref(x)); //在wait或get時運行 

auto f8 = std::async(std::launch::async | std::launch::deferred, 
	baz, std::ref(x));//由具體實現來抉擇,同f5


int main()
{
	f7.wait();

	// 先去執行其他操作,再通過get()獲取std::async操作結果
	f7.get();

	system("pause");
	return 0;
}

2.配合任務std::packaged_task

std::packaged_task包裝任何可調用對象(函數、lambda表達式、bind表達式或其他函數對象),使得能異步調用它。其返回值或所拋出異常被存儲於通過std::future對象訪問的共享狀態中。std::packaged_task對象是一個可調用對象,可以被封裝入一個std::function對象,可以作爲線程函數傳給std::thread,或作爲參數傳遞並回調,或者直接調用。其返回值作爲異步結果,存儲在由get_future()獲取的std::future中。

#include <iostream>
#include <thread>
#include <future>
#include <functional>
#include <deque>

int mul(int a,int b)
{
	return a * b;
}

//std::packaged_task包裝任何可調用對象(函數、lambda表達式、bind表達式或其他函數對象)

void task_lambda()
{
	std::packaged_task<int(int, int)> task([](int a,int b) {
		return mul(a, b);
	});
	std::future<int> result = task.get_future();
	task(2, 3); //2*3=6
	std::cout << "task_lambda:2*3=" << result.get() << std::endl;
}

void task_bind()
{
	std::packaged_task<int(void)> task(std::bind(mul, 2, 3));
	std::future<int> result = task.get_future();
	task();
	std::cout << "task_bind:2*3=" << result.get() << std::endl;
}

void task_thread()
{
	std::packaged_task<int(int, int)> task(mul);
	std::future<int> result = task.get_future();
	std::thread task_td(std::move(task), 2, 3);
	task_td.join();
	std::cout << "task_thread:2*3=" << result.get() << std::endl;
}

void task_deque()
{
	std::deque<std::packaged_task<int(void)>> task_q;
	std::packaged_task<int(void)> t1(std::bind(mul, 2, 3));
	std::packaged_task<int(void)> t2(std::bind(mul, 2, 4));
	std::packaged_task<int(void)> t3(std::bind(mul, 2, 5));
	task_q.push_back(std::move(t1));
	task_q.push_back(std::move(t2));
	task_q.push_back(std::move(t3));
	//... 
	task_q.front()();
	//...
	std::future<int> result = task_q.front().get_future();
	task_q.pop_front();
	std::cout << "task_deque:2*3=" << result.get() << std::endl;
}

int main()
{
	task_lambda();
	task_bind();
	task_thread();
	task_deque();

	system("pause");
	return 0;
}

3.配合std::promise

std::promise<T>提供一種設置值(類型T)的方式,它可以在這之後通過相關聯的std::future<T>對象進行讀取。一對std::promise/std::future(可以通過get_future()函數獲取promise相關的future對象),線程A通過設置promise的值(使用set_value()函數)使future變爲就緒狀態,線程B阻塞future待其就緒後可以獲取所存儲的值。如果銷燬std::promise時未設置值,則將存入一個異常。

#include <vector>
#include <thread>
#include <future>
//數值操作庫
#include <numeric> 
#include <iostream>
//日期時間
#include <chrono> 

void accumulate(std::vector<int>::iterator begin
,std::vector<int>::iterator end
,std::promise<int> accumulate_promise)
{
	const int sum = std::accumulate(begin, end, 0); //求累加和,0爲初始值
	std::this_thread::sleep_for(std::chrono::seconds(1));
	accumulate_promise.set_value(sum); //提醒future
}

void do_work(std::promise<void> barrier)
{
	std::this_thread::sleep_for(std::chrono::seconds(1));
	barrier.set_value();
	std::cout << "do_work end\n";
}

int main()
{
	//演示用promise<int>在線程間傳遞結果
	std::vector<int> numbers(10);//size=10
	std::iota(numbers.begin(), numbers.end(), 0);//從0開始遞增填充
	std::promise<int> accumulate_promise;
	std::future<int> accumulate_future = accumulate_promise.get_future();
	std::thread work_thread(accumulate,numbers.begin(),numbers.end(),
		std::move(accumulate_promise));

	//future::get()將等待直至該future擁有合法結果並取得他
	//無需再get()前調用wait()
	//accumulate_future.wait(); //等待結果
	std::cout << "result=" << accumulate_future.get() << std::endl;
	work_thread.join(); //等待線程中操作結束

	//演示用promise<void>在線程間對狀態發信號
	std::promise<void> barrier;
	std::future<void> barrier_future = barrier.get_future();
	std::thread do_work_thread(do_work, std::move(barrier));
	barrier_future.wait();
	std::cout << "do_work_thread join\n";
	do_work_thread.join();

	system("pause");
	return 0;
}

4.保存異常

如果std::async或std::packaged_task時,異常會被存儲於future中,代替所存儲的值,future變爲就緒狀態,在調用get()時會重新引發所存儲的異常。std::promise如果期望存儲一個異常,則調用set_exception()而不是set_value(),

extern std::promise<double> some_promise;

try {
	//計算value
	some_promise.set_value(value);
}
catch (...) {
	//std::current_exception()獲取已引發的異常
	some_promise.set_exception(std::current_exception());
	//std::copy_exception()存儲新的異常
	//some_promise.set_exception(std::copy_exception(std::logic_error("f")));
}

另一種情況是,當future還未就緒,future關聯的std::promise或std::packaged_task的析構函數會將具有std::future_errc::broken_promise錯誤代碼的std::future_error異常存儲在相關聯的狀態中。

5.等待自多個線程

如果從多個線程訪問單個std::future對象而不進行額外的同步,就會出現數據競爭和未定義的行爲。並且,std::future調用一次get()後,就沒有任何可獲取的值留下了。

如果要求多個線程等待同一個事件,可以使用std::shared_future。std::future是可移動的,所有權可以在實例間轉移,但是一次只有一個實例指向特定的異步結果。std::shared_future實例是可複製的,因此可以有多個對象引用同一個相關狀態。對於單個std::shared_future實例,仍然需要鎖來保護訪問。如果每個線程都通過自己的std::shared_future對象去訪問該狀態,那麼就是安全的。

參考手冊上的例子,通過一個單獨的promise關聯shared_future,等待兩個線程的任務都完成:

#include <iostream>
#include <future>
#include <chrono>

int main()
{
	std::promise<void> ready_promise, t1_ready_promise, t2_ready_promise;
	std::shared_future<void> ready_future(ready_promise.get_future());

	std::chrono::time_point<std::chrono::high_resolution_clock> start;

	auto fun1 = [&, ready_future]() -> std::chrono::duration<double, std::milli>
	{
		t1_ready_promise.set_value();
		ready_future.wait(); // 等待來自 main() 的信號
		return std::chrono::high_resolution_clock::now() - start;
	};

	auto fun2 = [&, ready_future]() -> std::chrono::duration<double, std::milli>
	{
		t2_ready_promise.set_value();
		ready_future.wait(); // 等待來自 main() 的信號
		return std::chrono::high_resolution_clock::now() - start;
	};

	auto result1 = std::async(std::launch::async, fun1);
	auto result2 = std::async(std::launch::async, fun2);

	// 等待線程變爲就緒
	t1_ready_promise.get_future().wait();
	t2_ready_promise.get_future().wait();

	// 線程已就緒,開始時鐘
	start = std::chrono::high_resolution_clock::now();

	// 向線程發信使之運行
	ready_promise.set_value();

	std::cout << "Thread 1 received the signal "
		<< result1.get().count() << " ms after start\n"
		<< "Thread 2 received the signal "
		<< result2.get().count() << " ms after start\n";

	system("pause");
	return 0;
}

 

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