libco 协程(2)细说协程的应用场景
名词介绍
同步
当我们要买一本书,但是淘宝上没有,我们选择一直刷新界面直到出现这本书。这种就叫做同步。
也就是说,如果我们需要的事件没有到来我们需要一直等下去。
异步
我们要买口罩但是药店没有,这时候我们拜托店长等到有口罩的时候打电话通知我们去买,然后我们继续去忙其他事,这就叫做异步。
通常,异步的实现是向操作系统注册一个回调函数,当事件发生的时候调用回调函数。
I/O复用模型(事件驱动模型)
现在常见的I/O复用模型(如EPOLL,SELECT)都是通过事件回调的方式实现异步操作。具体关于I/O复用模型的内容在Linux I/O模型
以epoll为例,当事件到来的时候我们对相应的套接字进行read()操作,此时在系统同步调用中,是需要将系统缓冲区的数据拷贝到用户缓冲区中后才会返回,就是一直阻塞在这里。对于单线程程序来说,此时就好像卡住了一样。
那么如何解决这样的情况?
引入多线程/多进程
因为处理数据的函数,都有可能造成阻塞导致整个程序卡住,那么我们可以用另外一个线程/进程来处理这个函数避免主线程卡住导致后续的请求得不到正确的处理。
主要的核心思想是: 一个线程(主线程)负责监听和分配需要处理的文件描述符,其他的线程去处理实际的数据逻辑,相互不干扰,从而实现了高并发。
问题
看似完美的方案实际上并不是很完美,首先我们要知道有当连接请求到来的时候我们需要并发的去处理相关请求,处理的线程有会有几个原因导致他不能一直进行下去:
等待客户端发数据
数据库读取,因为数据库保存在硬盘之上,硬盘再快也比内存慢,因此从硬盘读取数据出来就会要等。
线程本身需要调用另一个服务器上的数据库/API才能获得数据。
以上这几个原因都会导致我们线程卡住。尽管对于第一个问题可以用池化技术,直到有读请求到来的时候才分配线程进行读取,但是当没有请求的时候线程仍然存在仍然需要分配CPU,这样一来会大量浪费CPU的性能。
协程
协程相对于其他线程/进程而言,他是一种用户态线程,需要用户手动进行切换调度的。也就是说当协程写入阻塞的时候会导致所在的整个线程都阻塞住,我们所要做的就是要在阻塞前将协程切换出去,等到所请求的I/O非阻塞的时候再切换回来。
协程+I/O复用模型
对于上述的方案,我们要如何知道I/O请求是非阻塞的呢? 对于所有的I/O无非就是:硬盘请求的读写,网络请求的读写。所有的读写都设置为非阻塞式的,同时在系统中注册相应的回调函数,当系统将这些请求处理完毕以后,再通过回调函数通知用户处理。
这样一来,对于一个单线程-多协程的服务器而言,当有大量的请求到来的时候,其浪费在阻塞I/O上面的时间很少,而且相比线程而言避免了CPU线程间切换的开销。
协程的真正目的
其实协程的真正目的并不是为了去替换线程的使用,线程有线程的应用层场景,协程有协程的。真正的目的是为了让我们可以像使用同步的方法去写异步的调用。
怎么说呢? 还是回到刚刚服务器高并发的问题,其实最好的效率是将所有请求都注册一个处理的回调函数,直接在回调函数中进行操作就可以避免超级多的麻烦,但是更麻烦的事情却是这个注册回调函数本身。
首先是这样完全异步的方法使得我们阅读代码很麻烦,不能像看文章一样顺序感极强,而是这里一个那里一块。
其次就是在回调函数里面我们需要依靠这段数据获取另外一段数据,怎么办?继续回调。以此类推,回调函数的层次越多越影响代码的维护。
其实本来第二篇打算写协程的封装,但是后面仔细一寻思怎么用在我之前的服务器上呢,怎么想都很奇怪,后来去反复查询协程的应用场景和如何使用,这才发现协程用在服务器上面是要和系统回调绑定起来用的,这样就可以在一个费时操作的时候及时切换出去,实现调度,避免一个协程占用线程太长时间导致其他协程得不到相应,从表现上看起来像是卡住了一样