C++ 并发编程指南(8)future与promise

本文详细介绍了C++标准库中的std::future、std::shared_future、std::async和std::packaged_task,包括它们的模板声明、成员方法、应用场景和异步操作的处理方式,帮助读者理解如何在多线程编程中有效地使用这些工具进行线程同步和异步结果获取。
摘要由CSDN通过智能技术生成


前言

条件变量在多个线程等待同一个事件的场合比较有用,如果等待线程只打算等待一次,那么当条件为true时它就不在等待这个条件变量了,条件变量未必是同步机制的最佳选择,在这个场景下使用期值(future) 可能会更合适,C++ 标准库使用std::future为这类一次性事件建模, 将这种一次性事件称为期望(future)std::future通常与std::asyncstd::packaged_taskstd::promise一起使用。

一、std::future

1.1、模版声明

在C++多线程编程中,同步线程间的操作通常是一个关键问题。C++11引入了std::future模版,提供了一种获取异步调用结果的机制,即:使用std::future对象表示异步操作的结果,模版声明如下:

template< class T > class future;

1.2、成员方法

  • constructor:构造函数、创建一个std::future对象
    • 1)不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它
    • 2)禁用拷贝构造
    • 3)支持移动构造
  • destructor:析构函数,释放 std::future对象
  • operator=:移动future对象
    • 1)禁用拷贝赋值
    • 2)支持移动赋值
  • get():获取异步调用的结果
    • 1)当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)
    • 2)如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪
    • 3)当共享状态就绪后,则该函数将取消阻塞并返回或抛出异常释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次
    • 4)共享状态是作为原子操作被访问
  • wait():等待异步调用返回结果
    • 1)等待共享状态就绪
    • 2)如果共享状态尚未就绪,则该函数将阻塞调用的线程直到就绪
    • 3)当共享状态就绪后,则该函数将取消阻塞并void返回
  • wait_for():等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回
  • wait_until():等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回
  • share():把共享状态从*this转移到shared_future对象,并返回shared_future对象
  • valid():检查 future 是否拥有共享状态

1.3、应用示例

假设你有一个长期运行的计算,预期最终将得到一个有用的结果,但是现在你还不需要这个值。你可以启动一个新的线程来执行该计算,这也意味着你必须注意将结果传回来,但是 std::thread 并没有提供直接的机制来这样做。实际上可以使用 std::async 来启动一个异步任务。std::async返回一个 std::future 对象,而不是给你一个std::thread对象让你在上面等待,std::thread 对象最终将持有函数的返回值。当你需要这个值时,只要在 std::thread对象上调用 get(),线程就会阻塞直到值返回,如下:

#include <future>
#include <iostream>
#include <unistd.h>

int main()
{
    // future from an async()
    std::future<int> f = std::async(std::launch::async, []{ sleep(5); return 8; });

    std::cout << " do something else..." << std::endl;
    
    f.wait();

    std::cout << "Done!\nResults are: " << f.get() << std::endl;
}

二、std::shared_future

2.1、模版声明

std::shared_future模版提供的功能与std::future类似,除了允许多个线程等候同一共享状态。std::shared_future可复制而且多个shared_future对象能指代同一共享状态。若每个线程通过其自身的shared_future对象副本访问,则从多个线程访问同一共享状态是安全的,模版声明如下:

template< class T > class shared_future;

2.2、应用示例

std::shared_future可用于同时向多个线程发信,类似 std::condition_variable::notify_all(),如下:

#include <iostream>
#include <thread>
#include <future>

void promise_string(std::promise<std::string> &pr)
{
    for (int i = 0; i < 50; i++)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    std::string str = "the current function name is: ";
    str.append(__FUNCTION__);
    pr.set_value(str);
}

int main()
{
    std::promise<std::string> pr;
    std::future<std::string> fu(pr.get_future());
    std::shared_future<std::string> sfu(fu.share());

    std::thread t1([&sfu]() {
        std::string str = sfu.get();
        std::cout << "thread1 function is: " << str.c_str() << std::endl;
    });

    std::thread t2([&sfu]() {
        std::string str = sfu.get();
        std::cout << "thread2 function is: " << str.c_str() << std::endl;
    });

    std::async(std::launch::async, [&pr]() {
        promise_string(pr);
    });

    t1.join();
    t2.join();
    system("pause");
}

三、std::async

3.1、模版声明

C++11 中提供了std::async模版用来创建异步任务 ,并通过std::future获取异步调用的结果。std::async模版的第一个参数是可调用实体,同时支持传入一些参数,模版声明如下:

template< class Function, class... Args >
std::future<typename std::result_of<typename std::decay<Function>::type(
        typename std::decay<Args>::type...)>::type>
    async( std::launch policy, Function&& f, Args&&... args );

3.2、模版参数

  • policy:启动策略,是std::launch枚举类型
  • f:可调用实体
  • args:传入的参数

3.3、启动策略

async函数接受两种不同的启动策略,这些策略在std::launch枚举中定义,如下:

  • std::launch::defered:这种策略意味着任务将在调用future::get()future::wait函数时延迟执行,也就是任务将在需要结果时同步执行
  • std::launch::async:任务在单独一个线程上异步执行

默认情况下async使用std::launch::defered | std::launch::async策略,这意味着任务可能异步执行,也可能延迟执行,具体取决于实现,示例:

#include <iostream>
#include <thread>
#include <future>
using namespace std;

thread::id  test_async() {
	this_thread::sleep_for(chrono::milliseconds(500));
	return this_thread::get_id();
}

thread::id  test_async_deferred() {
	this_thread::sleep_for(chrono::milliseconds(500));
	return this_thread::get_id();
}

int main() {
    // 另起一个线程去运行test_async
	future<thread::id> ans = std::async(launch::async, test_async);
	// 还没有运行test_async_deferred
	future<thread::id> ans_def = std::async(launch::deferred,test_async_deferred); //还没有运行test_async_deferred

	cout << "main thread id = " << this_thread::get_id() << endl;
	// 如果test_async这时还未运行完,程序会阻塞在这里,直到test_async运行结束返回
	cout << "test_async thread id = " << ans.get() << endl;
	// 这时候才去调用test_async_deferred,程序会阻塞在这里,直到test_async_deferred运行返回
	cout << "test_async_deferred thread id =  = " << ans_def.get() << endl;

	return 0;
}

输出结果

main thread id = 1
test_async thread id = 2
test_async_deferred thread id =  = 1

Process returned 0 (0x0)   execution time : 1.387 s
Press any key to continue.

从输出结果可以看出采用std::launch::defered策略时任务是同步执行,采用launch::async策略时任务是异步执行。

四、std::packaged_task

4.1、模版声明

std::packaged_task模版包装了可调用实体,返回值保存到与之相关联的std::future,包括函数运行时产生的异常。与std::promise一样,std::packaged_task支持move,但不支持拷贝copy,模版声明如下:

template< class R, class ...ArgTypes >
class packaged_task<R(ArgTypes...)>;

4.2、成员方法

  • valid():判断std::packaged_task是否拥有可调用实体
  • get_future():获取相关联的std::future对象
  • operator():执行可调用实体
  • make_ready_at_thread_exit():接收的参数与operator()(_ArgTypes...)一样,行为也一样。只有一点差别,那就是不会将计算结果立刻反馈给std::future,而是在其执行时所在的线程结束后,std::future::get才会取得结果
  • reset():重置状态,抛弃任何先前执行的存储结果

注意:关联的std::future可以通过get_future()获取到,get_future()仅能调用一次,多次调用会触发std::future_error异常。

4.3、std::packaged_task::valid()

通过packaged_task::valid()判断std::packaged_task对象是否是有效状态。当通过缺省构造初始化时,由于其未设置任何可调用对象,valid()返回false。只有当std::packaged_task设置了有效的函数或可调用对象,valid()才返回true。例如:

#include <future>   // std::packaged_task, std::future
#include <iostream> // std::cout

int main() {
    std::packaged_task<void()> task; // 缺省构造、默认构造
    std::cout << std::boolalpha << task.valid() << std::endl; // false

    std::packaged_task<void()> task2(std::move(task)); // 右值构造
    std::cout << std::boolalpha << task.valid() << std::endl; // false

    task = std::packaged_task<void()>([](){});  // 右值赋值, 可调用对象
    std::cout << std::boolalpha << task.valid() << std::endl; // true

    return 0;
}

4.4、std::packaged_task::operator()

调用std::packaged_task对象所封装可调用对象R,但其函数原型与R稍有不同:

void operator()(ArgTypes... );

operator()的返回值是void,即无返回值。因为std::packaged_task的设计主要是用来进行异步调用,因此R(ArgTypes...)的计算结果是通过std::future::get来获取的。该函数会将R的计算结果反馈给std::future,即使R抛出异常(此时std::future::get也会抛出同样的异常)。

#include <future>   // std::packaged_task, std::future
#include <iostream> // std::cout

int main() {
    std::packaged_task<void()> convert([](){
        throw std::logic_error("will catch in future");
    });
    std::future<void> future = convert.get_future();

    convert(); // 异常不会在此处抛出

    try {
        future.get();
    } catch(std::logic_error &e) {
        std::cerr << typeid(e).name() << ": " << e.what() << std::endl;
    }

    return 0;
}

4.5、std::packaged_task::reset()

std::promise不一样, std::promise仅可以执行一次set_valueset_exception函数,但std::packagged_task可以执行多次,其奥秘就是reset函数

template<class _Rp, class ..._ArgTypes>
void packaged_task<_Rp(_ArgTypes...)>::reset()
{
    if (!valid())
        __throw_future_error(future_errc::no_state);
    __p_ = promise<result_type>();
}

通过重新构造一个promise来达到多次调用的目的。显然调用reset后,需要重新get_future,以便获取下次operator()执行的结果。由于是重新构造了promise,因此reset操作并不会影响之前调用的make_ready_at_thread_exit结果,也即之前的定制的行为在线程退出时仍会发生。

五、std::promise

5.1、模版声明

std::promise模版保存了一个值或异常,保存的值可以被关联的std::future读取。std::promise允许move语义(右值构造,右值赋值),但不允许拷贝(拷贝构造、赋值),std::future亦然。std::promisestd::future合作共同实现了多线程间通信,模版声明如下:

template<class _Ty>
	class promise

5.2、成员方法

  • get_future():获取与std::promise相关联的std::future对象
  • set_value():给std::promise设置值
  • set_value_at_thread_exit()std::promise所在线程退出时,std::future收到通过该函数设置的值
  • set_exception_at_thread_exitstd::promise所在线程退出时,std::future则抛出该函数指定的异常

注意:一个std::promise实例只能与一个std::future关联共享状态,当在同一个std::promise上反复调用get_future()会抛出future_error异常。

5.3、设置std::promise的值

通过成员函数set_value可以设置std::promise中保存的值,该值最终会被与之关联的std::future::get读取到。

#include <iostream>
#include <thread>
#include <string>
#include <future>
#include <chrono>
using namespace std::chrono;

void read(std::future<std::string> *future) {
    // future会一直阻塞,直到有值到来
    std::cout << future->get() << std::endl;
}

int main() {
    // promise 相当于生产者
    std::promise<std::string> promise;
    // future 相当于消费者, 右值构造
    std::future<std::string> future = promise.get_future();
    // 另一线程中通过future来读取promise的值
    std::thread thread(read, &future);
    // 让当前的线程等待一段时间
    std::this_thread::sleep_for(seconds(2));
    // 设置值给promise
    promise.set_value("hello future");
    // 等待线程执行完成
    thread.join();

    return 0;
}

注意set_value只能被调用一次,多次调用会抛出std::future_error异常。std::promise::set_xxx函数会改变std::promise的状态为ready,再次调用时发现状态已要是ready了,则抛出异常。

5.4、std::promise不设置值

如果std::promise直到销毁时,都未设置过任何值,则std::promise会在析构时自动设置为std::future_error,这会导致std::future.get()调用抛出std::future_error异常,如下:

#include <iostream> // std::cout, std::endl
#include <thread>   // std::thread
#include <future>   // std::promise, std::future
#include <chrono>   // seconds
using namespace std::chrono;

void read(std::future<int> future) {
    try {
        future.get();
    }
    catch (std::future_error &e) {
        std::cerr << e.code() << "\n" << e.what() << std::endl;
    }
}

int main() {
    std::thread thread;
    {
        std::promise<int> promise;
        thread = std::thread(read, promise.get_future());
    }

    thread.join();
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++11 标准提供了一些并发编程指南,帮助程序员处理并发编程中的常见问题。这些指南可以帮助开发者实现高效、可靠的多线程应用。 首先,C++11 引入了 std::thread 类,它是一种线程的表示方式,可以方便地创建和管理线程。通过 std::thread,我们可以启动一个新线程并指定要执行的函数或函数对象。此外,std::thread 还提供了一系列的成员函数,如 join() 和 detach(),用于等待线程结束或分离线程。 其次,C++11 还引入了 std::mutex 和 std::lock_guard 类,用于解决多线程下的竞争条件问题。std::mutex 是一种互斥量,可以通过调用 lock() 和 unlock() 来控制对共享资源的访问。std::lock_guard 是一种锁保护类型,它的构造函数会自动在当前作用域上锁,析构函数会自动解锁,确保锁的正确使用。 此外,C++11 提供了 std::condition_variable 类,用于实现多线程间的条件变量通信。std::condition_variable 允许线程等待某个条件的发生,并在条件满足时由其他线程进行通知。 还有一个重要的概念是原子操作,C++11 提供了 std::atomic 类模板来实现无锁编程。通过 std::atomic,我们可以对共享变量进行原子操作,避免了需要锁保护的临界区域。 最后,C++11 还引入了 std::future 类模板和 std::promise 类模板,用于实现异步计算和线程间的数据传递。std::future 可以保存一个异步操作(如函数调用)的结果,而 std::promise 则可以在某个时间点设置这个结果。 综上所述,C++11 并发指南中的一些关键特性包括 std::thread、std::mutex、std::lock_guard、std::condition_variable、std::atomic、std::future 和 std::promise。它们为我们提供了一些基本工具和机制,帮助我们更加方便地编写多线程应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值