add 7. concurrency

This commit is contained in:
johnwdjiang 2020-06-24 18:40:53 +08:00
parent 0788bd3685
commit 87c58e9286

View File

@ -0,0 +1,35 @@
# 优先基于任务编程而不是基于线程
如果开发者想要异步执行 `doAsyncWork` 函数,通常有两种方式。其一是通过创建 `std::thread` 执行 `doAsyncWork` 比如
```cpp
int doAsyncWork();
std::thread t(doAsyncWork);
```
其二是将 `doAsyncWork` 传递给 `std::async` 一种基于任务的策略:
```cpp
auto fut = std::async(doAsyncWork); // "fut" for "future"
```
这种方式中,函数对象作为一个任务传递给 `std::async`
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设唤醒`doAsyncWork`的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法可以简单地获取`std::async`返回的`future`提供的`get`函数获取这个返回值。如果`doAsycnWork`发生了异常,`get`函数就显得更为重要,因为`get`函数可以提供抛出异常的访问,而基于线程的方法,如果`doAsyncWork`抛出了异常,线程会直接终止(通过调用`std::terminate`)。
基于线程与基于任务最根本的区别在于抽象层次的高低。基于任务的方式使得开发者从线程管理的细节中解放出来对此在C++并发软件中总结了'thread'的三种含义:
- 硬件线程Hardware threads是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。
- 软件线程Software threads也被称为系统线程是操作系统管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程因为当软件线程被比如 I/O、同步锁或者条件变量阻塞的时候操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。
- `std::threads`是C++执行过程的对象并作为软件线程的handle(句柄)。`std::threads`存在多种状态1. `null`表示空句柄,因为处于默认构造状态(即没有函数来执行),因此不对应任何软件线程。 2. moved from (moved-to的`std::thread`就对应软件进程开始执行) 3. `joined`(连接唤醒与被唤醒的两个线程) 4. `detached`(将两个连接的线程分离)
软件线程是有限的资源。如果开发者试图创建大于系统支持的硬件线程数量,会抛出`std::system_error`异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 `doAsyncWork``noexcept`
```cpp
int doAsyncWork() noexcept; // see Item 14 for noexcept
```
这段代码仍然会抛出异常。
```cpp
std::thread t(doAsyncWork); // throw if no more
// threads are available
```
设计良好的软件必须有效地处理这种可能性(软件线程资源耗尽),一种有效的方法是在当前线程执行`doAsyncWork`但是这可能会导致负载不均而且如果当前线程是GUI线程可能会导致响应时间过长的问题另一种方法是等待当前运行的线程结束之后再创建新的线程但是仍然有可能当前运行的线程在等待`doAsyncWork`的结果(例如操作得到的变量或者条件变量的通知)。
即使没有超出软件线程的限额仍然可能会遇到资源超额的麻烦。如果当前准备运行的软件线程大于硬件线程的数量系统的线程调度程序会将硬件核心的时间切片当一个软件线程的时间片执行结束会让给另一个软件线程即发生上下文切换。软件线程的上下文切换会增加系统的软件线程管理开销并且如果发生了硬件核心漂移这个开销会更高具体来说如果发生了硬件核心漂移1CPU cache中关于上次执行线程的数据很少需要重新加载指令2新线程的cache数据会覆盖老线程的数据如果将来会再次覆盖老线程的数据显然频繁覆盖增加很多切换开销。
避免资源超额是困难的,因为软件线程之于硬件线程的最佳比例取决于软件线程的执行频率,