C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第1张图片

  • 上图是线程创建的相关接口:
    • ①底层接口thread让我们可以启动线程。为了“返回数据”,我们需要可共享的变量(global或static变量,或是以实参传递的变量)。为了“返回异常”,可利用类型std::exception_ptr(它被std::current_exception()返回并可被std::rethrow_exception()处理)
    • ②Shared state的概念使我们可以能够以一种较便捷的方法处理返回值或异常。搭配底层接口锁提供的promise我们可以建立一个shared state然后通过一个future来处理它
    • ③在高级层面,packaged_task或asyhc()会自动建立一个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()的三种标准使用方法:

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第2张图片

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第3张图片

  • 将发射策略std::launch::async|std::launch::deferred传递给async(),其结果和不传递任何发射策略是一样的。发射策略设为0会导致不可预期的行为

二、细说future

  • future<>用来表示某一操作的成员,成果可能是返回值或是异常。future<>可以绑定于std::saync()、或一个std::packaged_task或一个promise上
  • future<>支持的操作如下:

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第4张图片

  • 调用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 sgate的最后拥有者,而相关的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(),它们被内部用来在线程之间传递异常、建立异常拷贝。然而拷贝使成本变高

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第5张图片

四、细说std::promise

  • promise,其对象用来临时持有一个值或一个异常
  • 一般而言,promise可持有一个shared state,一旦shared state持有了一个值或一个异常,那么就说它是ready
  • 下图列出了promise所支持的所有操作:

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第6张图片

  • 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的操作函数:

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第7张图片

  • 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的操作函数:

C++(标准库):46---并发之(细说启动线程:async、future、shared_future、promise、packaged_task、thread)_第8张图片

  • 如果thread object关联至某个线程,它就是所谓koinable(可连接的)此情况下调用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

你可能感兴趣的:(C++(标准库))