C++标准库笔记-多线程-细说Future

Future

在前文C++11笔记-多线程-初识-async()和Future中,初次使用async()和Future来实现一个多线程的demo;
在上一文中讲到,class std::future<>允许你等待线程结束并获取其结果,可以获取到线程函数的返回值或者是一个异常;
这里将在学《C++标准库》时,把关于future的详细内容记录下来,方便以后工作学习使用;
在18.3.2 细说Future中,讲到future用来表现某一操作的成果(outcome):可能是一个返回值或是一个异常,但二者不会同时存在;这份成果被管理于一个shared state内,后者可以被async()或一个packaged_task或一个promise创建处理。这份成果也许尚未存在,因此future持有的也可能是“生成该成果”的每一件必要的东西;

接口

操作效果
future fDefault 构造函数,建立一个future,带着无效状态(invalid state)
future f(rv)Move构造函数,建立一个新的future,状态取自rv,并令rv状态失效
f.~future()销毁状态也销毁*this
f = rvMove assignment;销毁f的旧状态,取rv状态填之,并令rv状态失效
f.valid()如果f具备有效状态就获得true,然后你才可以调用以下各个成员函数
f.get()阻塞(block)直到后台操作完成。它会迫使被推迟的线程(deferred thread)同步启动(start synchronously),产出结果(如果有的话)或发出异常,并令其状态失效
f.wait()阻塞(block)直到后台操作完成。它会迫使被推迟的线程(deferred thread)同步启动(start synchronously)
f.wait_for(dur)阻塞(block)dur时间段,或直到后台操作完成。但被推迟的线程(deferred thread)并不会被强制启动
f.wait_until(tp)阻塞(block)直至到达时间点tp,或直到后台操作完成。但被推迟的线程(deferred thread)并不会被强制启动
f.share()产生一个shared_future带有当前状态,并令f的状态失效

注意事项

1.如果future是被async()返回且其相关的task推迟,对它调用get()或者wait()会同步启动该task,但是wait_for()和wait_until()都不会令一个被推迟任务(deferred task)启动;
2.future的成果只能被取出一次。因此future可能处于有效(valid)或无效(invalid)状态:有效意味着“某一操作的成果或爆发的异常”尚未被取出;
get()只能被调用一次,因为get()会令future处于无效状态;
面对无效的future,调用其析构函数、move assignment操作符或valid()以外的任何操作,都会导致不可预期的行为。
3.get()的返回值:

情况一如果它是void,get()获得的就是void,也就是之前说的“无物”
情况二如果future的template参数是个reference类型,get()便返回一个reference指向返回值;
情况三非上述两种情况,则get()返回返回值的一份copy,或是对返回值进行move assignment动作——取决于返回类型是否支持move assignment语义;

注意:get()的返回值取决于future<>的特化类型;
4.future既不提供copy构造函数也不提供copy assignment操作符,确保绝不会有两个object共享某一个后台操作的状态(state)。“将某个future object状态搬移至另一个”的唯一办法是:调用move构造函数或move assignment操作符;然而通过使用share_future object 也可以令后台任务的状态被共享,其share()会释出控制权;
5.如果调用析构函数的那个future是某一shared state的最后拥有者,而相关的task已启动但尚未结束,析构函数会造成阻塞(block),直到任务完成;

处理后台异常

如果 std::async 调用的函数抛出异常,那么这个异常会被存储在值的位置,同时 future 变为 ready ,如果调用 get() 会重新抛出存储的异常。
在《C++标准库》18.1节中的处理异常部分中举了一个栗子:启动一个后台任务,该任务使用无限循环持续的分配内存给list 变量v增加新元素,这样线程会出现内存分配异常(bad_alloc);
给出以下代码:

#include <future>
#include <list>
#include <iostream>
#include <exception>

using namespace std;

void task1()
{
    list<int> v;
	while (true)
	{
		for (size_t i = 0; i < 1000000; i++)
		{
			v.push_back(i);
		}
		cout.put('.').flush();
	}

	cout << "task 1 over " << endl;
}
int main()
{
	cout << "starting 2 task" << endl;
	cout << "- task1: process endless loop of memory consuption" << endl;
	cout << "- task2: wait for <return> and then for task1" << endl;

	auto f1 = async(task1);
	//cin.get();
	cout << "\nwait for the end of task1:" << endl;
	try
	{
		f1.get();
	}
	catch (const exception& e)
	{
		cout << "EXCEPTION: " << e.what() << endl;
	}
    system("pause");
}

执行后效果:
在这里插入图片描述
当async()开始启动task1后,task1函数会有两种情况出现;
一种情况是:好的消息,没有什么特殊的事情发生;
另外一种情况是:坏的消息,“对future调用get()”也能处理异常;事实上,当get()被调用,且后台操作已经(或随后由于异常)而终止,该异常不会在此线程内被处理,而是会再次被传播出去。因此,欲处理后台操作所生的异常,你只需要偕同get()做出“以同步方式调用该操作”所作出的相同动作即可;
这个无限循环迟早会出现异常,该异常会终止线程,因为它未被捕获。Future object会保持这一状态直到get()被调用。搭配get()后这个异常在main()内被进一步传播;

等待wait/wait_for/wait_until

wait

只要对某个future调用wait(),就可以强制启动该future象征的线程并等待这一后台操作终止;

	std::future<...> f(std::async(func));
	...
	f.wait();

wait_for

使用wait_for()并给予一个时间段,就可让“异步、运行中”的操作等待一段有限时间:

std::future<...> f(std::async(func));
...
// 等待func线程10s;
f.wait_for(std::chrono::seconds(10));

wait_until

使用wait_until()就可以等待直至达到某特定时间点;

std::future<...> f(std::async(func));
...
// 等待func线程,直到1分钟后;
f.wait_until(std::system_clock::now() + std::chrono::minutes(1));

wait_for和wait_until并不强制启动线程(如果线程尚未启动的话);

wait_for和wait_until的返回值

不论wait_for和wait_until有以下三种返回值:
1.std::future_status::deferred——如果async()延缓了操作而程序中又完全没有调用wait()或get()(那会强制启动)。这种情况下上述两个函数都会立刻返回;
2.std::future_status::timeout——如果某个操作被异步启动但尚未结束,而waiting又已逾期(对给定的时间段而言);
3.std::future_status::ready——如果操作已经完成;
注意:如果给wait_for或者wait_until传入一个zero时间段或者一个过去的时间点,那么wait_for或wait_until函数仅仅是查询这个后台任务是否已被启动,和/或是否它正在运行中;

例子1

《C++标准库》举了一个例子,显示了wait_until的应用场景;
需求:需要计算一个数值,计算时间或长或短,我们必须在某个时间段内获得一个粗精度的结果(quickComputation函数),如果这个时间段内能计算得到高精度的结果(accurateComputation)那就更好了;

int quickComputation();
int accurateComputation();
std::future<int> f;
int bestResultInTime()
{
	auto tp = std::chrono::system_clock::now() + std::chrono::minutes(1);
	f = std::async(std::launch::async, accurateComputation);
	int guess = quickComputation();
	std::future_status s = f.wait_until(tp);

	if (s == std::future_status::ready)
	{
		return f.get();
	}
	else
	{
		return guess;
	}
}

注意:future f不能是声明于bestResultInTime函数内的对象,那样的话若时间太短以至于无法完成accurateComputation(),future析构函数就会阻塞直到异步操作结束,这样bestResultInTime函数就会同时计算出粗精度结果和高精度结果,但返回值不一定是高精度的结果;

例子2

#include <future>
#include <exception>
#include <thread>
#include <chrono>
#include <random>
#include <iostream>
using namespace std;

void doSomthing(char c)
{
    default_random_engine dre(c);
    uniform_int_distribution<int> id(10, 1000);

    for (size_t i = 0; i < 10; i++)
    {
        this_thread::sleep_for(chrono::milliseconds(id(dre)));
        cout.put(c).flush();
    }
}

int main()
{
    cout << "starting 2 operations asynchronously" << endl;

    auto f1 = async([] {doSomthing('.'); });
    auto f2 = async([] {doSomthing('+'); });
    // 如果f1和f2两个任务中有一个没有被推迟执行,就需要轮询等待其完成;
    if (f1.wait_for(chrono::seconds(0)) != future_status::deferred
        || f2.wait_for(chrono::seconds(0)) != future_status::deferred)
    {
        // 轮询检查,f1或f2之一有完成;
        while (f1.wait_for(chrono::seconds(0)) != future_status::ready
            && f2.wait_for(chrono::seconds(0)) != future_status::ready)
        {
            this_thread::yield();
        }
    }
    cout.put('\n').flush();

    try
    {
        // 使用get,防止f1和f2有使用了std::launch::deferred
        f1.get();
        f2.get();
    }
    catch (const std::exception& e)
    {
        cout << "\nException:" << e.what() << endl;
    }
    cout << "\ndone" << endl;
    system("pause");
}

总结

async()提供了一种编程环境,让我们有机会并行启动某些“稍后(当get()被调用时)才会用到其结果”的动作。换句话说,如果你有某个独立函数f,你有可能受益于并行机制,做法是在你需要调用f时改而把f传给async(),然后在你需要f的结果时改为“对async()返回的future调用get()”;
future提供了外部对async()启动的线程的联系,可以获取到返回值或者异常,如果没有启动成功还能启动函数线程执行;

文献

1.《C++标准库》第二版;

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
原子类型和原子操作是并发编程中的重要概念。在多线程编程中,当多个线程同时操作同一个共享变量时,可能会出现数据竞争的问题,导致程序的结果不可预测。为了解决这个问题,C11引入了原子类型和原子操作。 原子类型是一种特殊的数据类型,它的操作都是原子的,即不会被其他线程的操作干扰。C11提供了几种原子类型,包括原子整型、原子指针和原子布尔型等。原子类型可以在多线程环境下安全地进行读写操作,保证数据的一致性。 原子操作是对原子类型进行的操作,包括赋值、递增、递减等。这些操作都是原子的,不会被其他线程的操作干扰。原子操作通过一些特殊的语法和函数来实现,如原子操作的语法是“Atomics”开头的函数。例如,使用原子操作可以通过atomic_store函数将一个值存储到原子类型的变量中,保证线程安全。 使用原子类型和原子操作可以简化并发编程的复杂度,避免数据竞争。原子类型和原子操作提供了一种高效的并发编程模型,在多线程编程中具有重要的应用价值。 在使用原子类型和原子操作时,需要注意一些问题。首先,原子操作虽然保证了操作的原子性,但并不能完全解决所有的并发问题。其次,原子操作的性能可能不如普通操作,因为原子操作需要保证线程安全,可能需要加锁等额外开销。 总之,原子类型和原子操作是C11提供的一种并发编程的解决方案。通过使用原子类型和原子操作,可以有效解决多线程编程中的数据竞争问题,提高程序的并发性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑山老妖的笔记本

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值