# Lab1 构建协程任务封装

## tinyCoroLab1 实验简介

本节我们将正式开始 tinyCoroLab1，即构建协程任务封装模块。对于 tinyCoro 所有的任务都会被封装成协程函数并由执行引擎执行，因此构建 tinyCoro 的第一步便是完成由协程作为底层支持的任务基本单元 task 的实现。

#### 预备知识

> ⚠️预备知识即在实验开始前你应该已经掌握的知识，且在**知识铺垫章节**中均有涉及

* **C++20 协程概念及使用**

## 📖lab1 任务书

### 实验前置讲解

本节实验涉及到的文件为[include/coro/task.hpp](https://github.com/sakurs2/tinyCoroLab/blob/master/include/coro/task.hpp)，实验者需要预先打开文件浏览大致代码结构，下面针对该文件内容进行讲解。

promise\_base 为 task 定义了 task 创建以及结束时的调度逻辑，分别由`initial_suspend()`和`final_suspend()`实现。`initial_suspend()`必须返回`std::suspend_always`，因为创建 task 后不能立即执行，必须交由执行引擎执行而不是创建者，而`final_suspend()`则交由实验者实现。

与 task 绑定的 promise 定义如下，因为 task 可以带有返回值所以需要添加一个模板参数指定返回值，同时继承 promise\_base 的调度逻辑，并且通过继承 container 来实现暂存返回值。

```cpp
template<typename return_type>
struct promise final : public promise_base, public container<return_type>
```

对于返回值为空的 task 不需要继承 container 且 C++ 协程对于返回值为空的情况也需要额外处理，因此特化一个模板参数为空的 promise，对于熟悉 C++20 协程的同学来说这是很常规的操作。

```cpp
template<>
struct promise<void> : public promise_base
```

> 💡**为何需要额外定义 promise\_base？** 因为 C++ 协程规定对于协程返回值是否为空编译器需要调用不同的函数，因此通常与协程关联的 promise 都会额外特化一个返回值模板参数为空的实现，而对于 tinyCoro 的 task 返回值是否为空不影响协程调度逻辑，因此抽取共性部分单独作为一个类来减少冗余代码。

对于 task 的定义不再做过多详细介绍，因为都是常规的协程实现，不过需要注意**当 task 内部 co\_await 另一个 task 后子 task 会立刻执行，tinyCoro 对于 task 的设计是只有当 task 创建或者 co\_await 特定的 awaiter 时才会陷入 suspend 状态**，这一设计的作用会在后续实验中体现。

> 💡**为何 task 没有支持 yield？** 现有的功能场景下 yield 并没有什么作用，比较鸡肋就不再实现，当然实验者可以自行拓展。

如果实验者不理解上述讲解的内容可以查看前几章的[C++ 协程入门教程](https://sakurs2.gitbook.io/tinycorolab-docs/tinycorolab1/ch06_lab1)来学习铺垫知识。作为 tinyCoroLab 的开篇实验，lab1 难度设计较低，实验者仅需要在现有 task 的基础上完成一些子功能即可。

### ⚠️注意事项

* 请确保已阅读过**tinyCoroLab Introduce**章节。
* 为了确保正确实现目标函数，实验者可能需要做一些额外操作：新增类、修改现有类的实现、补充现有类的方法和成员变量等操作，请遵循**free-design 实验原则**。

### 实验任务书

#### 🧑‍💻Task #1 - 实现 task 执行结束后的正确调度逻辑

**任务目标**

前述小节提到过 task 运行结束后的调度逻辑是通过 promise\_base 的`final_suspend()`函数实现的，对于简单执行的协程仅仅返回`std::suspend_always`即可，但这样会出现什么问题呢？下图给出了演示：

![lab1\_case1](https://31725031-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FK1bCOplhXLsdtejXR66R%2Fuploads%2Fgit-blob-c5a934f92a0bb91f164c57ae23e35768756c6d16%2Flab1_case1.png?alt=media)

不难看出 task1 的部分执行逻辑被跳过了，正确的做法是 task2 在执行结束后将执行权转移给 task1 而不是 main，因此 tinyCoro 的 task 应当在被嵌套调用的情况下记录父级 task 的 handle 以便执行结束后利用该 handle 转移执行权。该功能主要由实验者在`final_suspend()`中负责实现。

> **💡为何在`final_suspend`返回`std::suspend_always`时 task2 在执行结束后执行权不会回到 task1？** 如果 task 关联的 promise 的`initial_suspend`函数返回的是`std::suspend_never`，那么 task2 在执行结束后执行权会回到 task1，此时 task1 是否继续执行由 co\_await 表达式产生的 awaiter 决定，但 tinyCoro 的 task 在被 co\_await 时，`initial_suspend`返回的是`std::suspend_always`，即该协程创建后立刻处于 suspend 状态，执行权回到父协程，随后 awaiter 中的`await_suspend`函数通过返回句柄来恢复子协程运行（同时父协程陷入 suspend 状态），子协程运行结束后由于父协程处于 suspend 状态，所以执行权会继续沿着调用栈向上转移，这一部分实验者一定要理解

**涉及文件**

* [include/coro/task.hpp](https://github.com/sakurs2/tinyCoroLab/blob/master/include/coro/task.hpp)

**待实现函数**

* `coro::detail::promise_base::final_suspend()`

#### Task #2 - 为 task 添加 detach 状态

对 C++ 协程编程较为熟悉的同学都知道，协程资源是需要外部调用函数主动进行释放的，如果没有及时释放会导致内存泄露的问题，对此一种通用解决方案是使用 RAII 的技巧来保管协程句柄，比如 tinyCoro 中的 task 会在析构时自动释放协程资源。

但是执行引擎是不会感知到 task 的存在的，向执行引擎提交 task 本质上是在提交 task 所持有的协程句柄。如果向执行引擎提交的是右值语义的 task，执行引擎会在获取 task 持有的协程句柄后调用`task.detach()`来使 task 处于 detach 状态，即 task 不再保管该协程句柄，如果外部想继续获取只会得到空句柄。

执行引擎让 task 处于 detach 状态也可以理解为将协程资源释放义务从 task 转移到执行引擎，当执行引擎将协程任务执行完毕后会调用`clean()`来释放协程资源，而 clean 函数需要根据入参协程句柄来判断该协程是否为 detach 状态，如果是 detach 状态，那么释放协程资源，否则就不执行任何逻辑，因为非 detach 状态的协程句柄此时一定是被某个 task 保管。

上述提到的`detach()`和`clean()`均需要由实验者实现。

> 💡**如果向执行引擎提交了左值类型的 task，随后 task 生命周期结束释放了协程句柄的资源，那么稍后执行引擎执行该协程岂不是会出现 core dump？** 是的，关于资源释放的问题可以有很复杂的情况，tinyCoro 在这方面并没有很完善，不过实验的测试程序不会涉及到这种 case，测试和实际使用基本都是以右值的形式提交 task。

**涉及文件**

* [include/coro/task.hpp](https://github.com/sakurs2/tinyCoroLab/blob/master/include/coro/task.hpp)

**待实现函数**

* `coro::task::detach()`
* `coro::clean(std::coroutine_handle<> handle)`

### 🔖测试

#### 功能测试

功能测试场景主要针对：

* **task 的正常执行**
* **task 的嵌套执行**
* **task 的移动语义**
* **协程资源的正确释放**

完成本节实验后，实验者请在构建目录下执行下列指令来构建以及运行测试程序：

```shell
make build-lab1 # 构建
make test-lab1 # 运行
```

#### 内存安全测试

在构建目录下运行下列指令来执行内存安全测试：

```shell
make memtest-lab1
```

测试通过会提示 pass，不通过会给出 valgrind 的输出文件位置，请实验者根据该文件排查内存故障。
