协程快速上手
什么是协程?
协程就是一个可以暂停和恢复执行的函数。别的函数是一镜到底,协程是走走停停。停下(被挂起)的时候,就是说当前线程可以去执行别的任务了,完了还能再回来
为什么要有协程
首先看看线程的历史。为什么会有线程呢,线程的作用是什么呢?
是为了尽可能多的吃满CPU,发挥性能。当然,这是我们说的多线程并发的常用场景。但是最开始引入线程其实是为了解决一个异步IO的问题。
一开始的电脑,程序员编辑完一个文件,按下保存,然后就可以去玩了。因为只有一个线程,在保存的时候,就不能干别的了。但是这显然是不合理的,磁盘在那吭哧吭哧写,但是cpu却是闲着的。闲着的cpu是否应该找点活干呢?
后来有了多任务的概念(应该不是当前场景首创,较真的话需要深入考究),也就是多个线程。有了多个线程,保存的线程阻塞了,不影响编辑的线程。这就是多线程带来的异步,让IO进行的时候,CPU能空出来。
…
随着计算机的发展,异步写磁盘的情况已经是小菜一碟了,但是又有了新的挑战,比如经典的C10K问题,也就是服务器单机1w并发请求处理。单个线程处理1w个请求必然会卡死。创建1w个线程,每个线程4M的栈,这就是40G的内存,那显然也不行。看来用线程实现的异步IO不中用了。
那是时候引入单线程异步IO的概念了。用一个线程,就可以发起很多个IO操作。这需要:
- 一个IO多路复用的通知机制。一个接口,通知所有的IO时间。如果不是多路复用的,那必然就需要新的线程
- 非阻塞IO(只需有IO发起操作,无需等IO完成)。阻塞了当前这个唯一的线程就被挂起了
todo: 简化一下
深入理解协程
理解协程,最好的办法就是和线程进行类比:
- 从调度、挂起切换角度比较
- 线程由OS调度、切换,调用阻塞IO等会被挂起,IO就绪OS会将其恢复
- 协程由用户调度、切换,由用户指定什么时候挂起,什么时候恢复。怎么指定呢?——通过
Awaiter
指定,await_suspend()
描述协程挂起时执行什么动作(比如发起一次非阻塞IO然后等待,比如注册fd到epoll然后等待);await_resume()
描述协程恢复时执行什么动作(比如读取就绪数据/向就绪fd发送)。
- 从编码角度比较。我们更习惯的都是以同步的方式编码
- 线程调用阻塞IO,比如read,发起一次IO,然后当前线程被OS阻塞,IO就绪时恢复。
- 协程效用一个异步IO/非阻塞IO,发起一次任务。然后当前协程被阻塞,任务就绪时恢复。线程阻不阻塞OS知道,协程怎么知道哪个IO要阻塞呢?用
co_await
标记。所以加上co_await后,就相当于告诉协程,这是一个“阻塞”IO,要阻塞协程。 - 简单来说,阻塞IO阻塞整个线程,非阻塞IO阻塞协程(用co_await标记上:这对于协程来说是一个阻塞IO)
- 从结果传出的角度
- 线程需要使用一个
std::promise
,传递不是立马能获取到的结果 - 协程也需要这么一个promise,它就是协程的
Task<T>
,里面要求必须有一个promise_type
,这就是协程的promise,用于获取不是立马得到的结果
- 线程需要使用一个
- 从第2点也可以看出:协程必须要搭配真正的异步IO才叫协程。因为
- 调用同步IO,OS管你这那的,直接阻塞整个线程了,它眼里可没有协程这么回事。
- 创建线程实现异步IO,那不还是多线程等于?
类比完再想想,什么叫做,协程时由用户负责调度切换的协程这句话,有没有更清晰?
C++20协程核心概念
promise_type
template<typename T>
struct Task<T>::promise_type {
T promised_value;
std::exception_ptr exptr;
T Get() {
if(exptr) std::rethrow_excetion(exptr);
return promised_value;
}
// 强制要求的函数:
void get_return_object(){ return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_never{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { exptr = std::current_excetion(); }
template<typename U = T>
require(std::is_same_v<U, void>)
void return_void() {}
template<typename U = T>
require(std::is_convertible_v<U, T>)
void return_value(U v){ promised_value = std::move(v); }
template<std::convertible_to<T> From>
std::suspend_always yield_value(From v) { promised_value = std::move(v); return {}; }
};
那么一个完整的task应该长什么样呢?
template<typename T>
class Task{
public:
struct promise_type;
using HandleType = std::coroutine_handle<promise_type>;
Task(HandleType handle): coro_handle_(handle){}
Task(Task&&);
Task& operator=(Task&&);
~Task(){ if(coro_handle_){ coro_handle_.destory(); coro_handle = nullptr; } }
Get() { return coro_handle_.promise().Get(); }
private:
HandleType coro_handle_;
};