关于回调与协程
最近在看muduo网络库,再读源码时给了我一点小小的回调震撼,而异步回调和协程都改变了串行的执行顺序,实现了并发,由此引发了以下思考。
注:并发是短时间内多个任务都在向前推进(无论顺序,执行了多少)所以可以是单核。
并行是指“同时”进行,也就是多核。
回调函数
在我看来,回调有通用、灵活、可定制的好处。
通用
通过给不同对象注册回调,可以减小不同对象间的耦合,给不同的功能任务分层,哪里出现问题,只需要在对应的对象里更改就好了,不对其他对象产生影响。
比如channel封装了fd,其最基础的的功能只有开启关闭监听事件类型,具体如何处理事件看给谁给它注册了callback。Acceptor就是accept;tcpconnect是read客户端传输的数据,读完再调用用户注册的回调;EventLoop是其他线程对当前线程的实时唤醒。
灵活
灵活性指的是不用把每一种可能的排列组合都写死在程序。read_event_for_Acceptor、read_event_for_EventLoop……以此类推,这样程序员就真成码农了。虽然模板元编程也能做到只编写一次代码,但它也是把实际的for_***的不同函数推导出来了,会造成代码膨胀。(而且思维没转过来之前模板元编程的可读性也很差)
可定制
可定制就是指在编写库时,可以暴露给用户注册回调的函数,来满足使用者的定制化功能。
说完好处,我就要开骂了。这玩意我算是初步理解什么叫回调地狱了,这我跟着别人整理好的带注释、精简版看,大体捋下来都花了一周。硬是看着他整理出的流程图、在全局ctrl+F找变量名、函数名,才看懂的。
协程
看了腾讯的libco协程库,其实相比说他是个协程库,他更像个非阻塞网络库。
它通过dlsym机制 hook 了各种网络 I/O 相关的系统调用,把诸如read()、write()和connect()等系统调用挂到epoll,把socket调用强制加上非阻塞,使得用户可以以“同步”的方式直接使用,把回调的被动使用,改为主动的“等待”(指被切走)。
其内部也是一个eventloop死循环,把监听到的活动事件和超时事件的回调加到active_list(还会有协程运行时cond_signal加入进来的事件)运行。相当于muduo抽象出的eventloop+channel。
超时事件: 是通过注册到时间轮,epoll每次返回推动时间获取的,以牺牲时间的精确度来提高效率。但我看代码没看懂,其中类的字段、继承关系,没注释,类名,变量名又缩略的太多,我真看不懂。在这贴一个能看懂的时间轮
通过它就不用再注册回调了,直接在Acceptor上accept,tcpconnect上read就行了,因为libco是非对称,有栈 协程,有"栈"(是广义上的栈数据结构,不是堆栈)存储协程创建运行顺序,就相当于把一个完整的线程栈切成了多个协程栈,代价是要切换上下文和栈空间,换来了以"同步"的思维方式编写代码,增加可读性。但在编写库的时候,还是需要用户注册回调函数,来实现定制化功能。(结构上也有可能是因为服务端是被动的所以需要回调)
关于c++20的协程是无栈协程,通过co_return co_yield和 co_await关键字来交互,用await切换上下文来代替有栈的调用关系,yield比await在切走前自动多执行一个函数。
但coroutine只是提供了上下文切换,调度结构的支持,还要用promise_type和返回类型中的await_ready、 await_suspend、 await_resume来自己制定调度规则,handle获得协程实例来获取中间状态,进行任意的调度。我要是想加到muduo里好像除了更改了回调的执行方式,还会加更多的工作量。(不过有人在写协程库co_context了,好像用起来挺方便的)