C++ 标准库 async、future、shared_future、promise、packaged_task、thread
原文链接:细说启动线程:async、future、shared_future、promise、packaged_task、thread
- 上图是线程创建的相关接口:
- ①底层接口thread让我们可以启动线程。为了“返回数据”,我们需要可共享的变量(global或static变量,或是以实参传递的变量)。为了“返回异常”,可利用类型std::exception_ptr(它被std::current_exception()返回并可被std::rethrow_exception()处理)
- ②Shared state的概念使我们可以能够以一种较便捷的方法处理返回值或异常。搭配底层接口锁提供的promise我们可以建立一个shared state然后通过一个future来处理它
- ③在高级层面,packaged_task或async()会自动建立一个shared state,它会因为一个return语句或一个未被捕获的异常而设置妥
- ④packaged_task允许我们建立一个“带着shared state”的object,但我们必须明确写出何时启动该线程
- ⑤若使用std::async(),我们无须关心何时真正启动。我们唯一确知的是当需要结果时就调用get()
- Shared State
- shared state允许“启动及控制后台机能”的object(一个promise、packaged task或async())能够和“处理其结果”的object相互沟通
- 因此,shared state必须能够持有被启动之目标函数以及某些状态和结果(一个返回值/一个异常)
- Shared state如果持有其函数运行结果,我们说它是ready(也就是返回值或异常已备妥待取)。Shared state通常被实现为一个reference-counted object——当它被最后一个使用者释放时即被销毁
一、细说async()
- 一般而言,std::async()是个辅助函数,用来在分离线程中启动某个函数。因此,如果底层平台支持多线程,你可以让函数并发运行;如果底层不支持多线程,也没有任何损失
- 然而async()的行为比较复杂,且高度取决于launch(发射)策略,后者可作为第一实参
- 下面是async()的三种标准使用方法:
- 将发射策略std::launch::async|std::launch::deferred传递给async(),其结果和不传递任何发射策略是一样的。发射策略设为0会导致不可预期的行为
二、细说future
- future<>用来表示某一操作的成员,成果可能是返回值或是异常。future<>可以绑定于std::saync()、或一个std::packaged_task或一个promise上
- future<>支持的操作如下:
- 调用future<>.get(),那么future<>绑定的成果只能被获取一次。get()的返回值取决于future<>的特化类型:
- 如果它是void,get()获得的就是void
- 如果future的template参数是个reference类型,get()返回一个reference指向返回值
- 否则get()返回返回值的一份copy,或是对返回值进行move assign操作
- 通过调用get()获取结果之后,future<>就处于无效状态,面对无效状态的future,调用其析构函数、move assignment操作符或valid()以外的任何操作,都会导致不可预期的行为。这种情况下C++标准推荐(但不强制)抛出一个future_error异常并带有差错码std::future_error::no_state
- future不提供copy构造函数或copy赋值运算符,确保绝不会有两个object共享某一个后台操作的状态。将“某个future状态搬移至另一个”的唯一办法是:调用move构造函数或move赋值运算符
- 如果调用析构函数的那个future是某一shared state的最后拥有者,而相关的task已启动但尚未结束,析构函数会阻塞,直到任务结束
- 使用shared_future可以领后台任务的状态被共享(见下)
三、细说shared_future
-
shared_future提供的语义的接口与future相同。但是其与future有以下的差异:
- ①允许多次调用get()
- ②支持copy语义
- ③get()是个const成员函数,返回一个const reference指向“shared state”中的值。但是future的get()返回的是non-const,返回一个move assigned拷贝
- ④不提供share()
-
因为sharef_future<>.get()返回的都是引用,因此有两点需要注意:
- ①确保在访问的时候,访问的数据的生命周期没有结束
- ②data race(数据竞争)
-
例如下面的代码:
- 一个线程获取async()的结果,如果有异常,那么捕获该异常并且改动该异常
- 如果另一个线程正在处理这个异常,上述代码会导致data race。为了解决这个问题,建议使用current_exception()和rethrow_exception(),它们被内部用来在线程之间传递异常、建立异常拷贝。然而拷贝使成本变高
四、细说std::promise
- promise,其对象用来临时持有一个值或一个异常
- 一般而言,promise可持有一个shared state,一旦shared state持有了一个值或一个异常,那么就说它是ready
- 下图列出了promise所支持的所有操作:
-
get_future()的注意事项:
- get_future()只能被调用一次,第二次调用会抛出std::future_error异常并带有差错码std::future_errc::future_already_retrieved
- 如果没有响应的shared state,则该调用会抛出std::future_error异常并带有差错码std::future_errc::no_state
- 所有用来设定数值或异常的成员函数都是线程安全的
五、细说std::packaged_task
- packaged_task被用来同时持有目标函数及其“成果”。“成果”也许是个返回值或是目标函数所触发的异常。你可以拿相应的目标函数来初始化packaged task,而后通过packaged task实施operator()调用该目标函数。最后,你可以对此packaged task取一个future以便处理器结果
- 下面是packaged_task的操作函数:
-
task函数与get_future()的相关注意事项:
- 如果调用task函数或调用get_future()却没有可用状态,会抛出std::future_error异常并带有差错码std::future_errc::no_state
- get_future()只能调用一次,如果第二次调用会抛出std::future_error异常并带有差错码std::future_errc::future_already_retrieved
- task函数只能调用一次,第二次调用task会抛出std::future_error异常并带有差错码std::future_errc::promise_already_satisfied
- 析构函数和reset()会抛弃shared state,意思是packaged task会释放shared state,并且如果shared state尚未被ready就令它变成ready,然后将一个std::future_error异常并带有差错码std::future_errc::broken_promise存储起来当做成果
- make_ready_at_thread_exit()函数用来确保在task的成果被处理之前,终结该task的线程的局部对象和其他材料会先被妥善清理
六、细说std::thread
- thread,其对象用来启动和表现线程
- 下图列出了thread的操作函数:
- 如果thread object关联至某个线程,它就是所谓joinable(可连接的)此情况下调用joinable(*)会获得true,调用get_id()可以获得thread ID
-
线程ID相关注意事项:
- 线程ID的数据类型为std::thread::id
- 我们可以对thread ID执行的操作是:进行比较,或者将其写到一个output stream中。此外不支持其他操作
- 有个hash函数用来在非定序(unordered)容器中管理thread ID
- detached thread不应该访问生命周期已经结束的object
-
thread还提供了一个static成员函数(如下图所示),用来查询并行线程可能的数量。注意事项如下:
- 返回可能的线程的数量
- 该数量只是个参考值,不保证准确
- 如果数量不可计算或不明确,返回值为0