事件驱动与协程:基本概念介绍

事件驱动与协程:基本概念介绍

协程是近几年非常流行而且被各种大厂所使用的微线程技术,比较好笑的是,他的概念出自上个世纪。因此,对于计算机历史有了解的同学,一定会听说过。


那么,为什么直到现在,协程才开始流行开来?协程又是个什么鬼东东?他解决了啥问题?今天我们就进行一趟深入浅出的讲解。


一)事件驱动

在讲明白协程之前,我们必须要讲明白什么是事件驱动,事件驱动模型或许这几年已经被大家说得烂了,听都不想听了。随着Nodejs的流行,大家都说,哎哟,事件驱动模型很牛逼,事件驱动模型非常快速blablabla。


实际上,事件驱动模型还有另外一个名字,而且更加出名的名字:io多路复用


1.1 io多路复用是啥?

噢,要讲明白io多路复用之前,我们还要知道一些概念,不要担心,我会用很傻逼的方式告诉你。


  • 小张找基友小鹏

小张第一次来到小鹏的宿舍楼,发现这个宿舍楼有一大堆的楼妈,为啥?因为学校竟然请了一堆楼妈来管理宿舍楼里的每一个宿舍,结果搞得一大堆楼妈唧唧咋咋的,乌烟瘴气。重要的就是学校会给每个楼妈都发工资。小张顿时觉得,这个学校没药救了。


没错,现实中已经没有这种傻逼现象了,学校也不会傻到这种程度为每一个宿舍都请一个楼妈来管理。


  • 过了几十年,小张的儿子来找小鹏的儿子

不巧,过了一二十年,小张的儿子和小鹏的儿子也上了同一所学校,小张张要来找小鹏鹏。小张张听了父亲说,这个学校的舍管特别傻逼,会有一堆,能烦死你。


结果小张张来到了宿舍楼,发现现在只有一个楼妈了,哎哟,不错嘛,学校终于没那么傻逼了。但是,这个楼妈比较傻,小张张去问她:小鹏鹏在哪个寝室啊?


她说,我也不知道啊。我带你上去找吧?


于是乎,小张张和楼妈挨个挨个的找宿舍,最终花了半天时间找到了小鹏鹏....(小张张尿了。


  • 又过去了差不多20年

此时,学校已经不是那个学校,张和鹏都挂得差不多了。新生代小春来找小丽,小春来到了这个宿舍,找到了楼妈。


这个楼妈就比较聪明了,每当一个学生入住新宿舍的时候,她就记录下这个人的名字,学号,电话,以及宿舍房号。当小春找小丽的时候,楼妈掏出眼镜,查表,马上就能知道小丽在哪里了,小春几分钟就到达了小丽的宿舍....



对应到编程届,在最开始的时候,为了实现一个服务器可以支持多个客户端连接,人们想出了fork/thread等办法,当一个连接来到的时候,就fork/thread一个进程/线程去接收并且处理请求,然而,当时估计是大家都穷吧,没啥钱买电脑,所以这个模型一直很好用,过去几十年都没有问题。


但是时代发展了,1980年代,计算机网络开始成型,越来越多的用户进行网络连接(其实也没多少),但是之前的fork/thread模型就不行了,太辣鸡了。(回想一下小张和小鹏与一大堆楼妈的故事


(select函数发明)1983年,人们终于意识到了这种傻逼问题,所以发明了一种叫做「IO多路复用」的模型,这种模型的好处就是「没必要开那么多条线程和进程了」,一个线程一个进程就搞定了。随着计算机业务的增长,这种IO多路复用的模型看似太傻逼了点,回想一下小张张和小鹏鹏:

  1. 宿舍楼里有可能有上百间宿舍
  2. 为了寻找到其中一间宿舍,你必须得一间一间去找,浪费时间


对应的编程模型就是:一个连接来了,就必须遍历所有已经注册的文件描述符,来找到那个需要处理信息的文件描述符,如果已经注册了几万个文件描述符,那会因为遍历这些已经注册的文件描述符,导致cpu爆炸。


直到2002年,互联网时代爆炸,数以千万计的请求在全世界范围内发来发去,服务器大爆炸,人们通过改进「IO多路复用」模型,进一步的优化,发明了一个叫做epoll的方法。这个方法就是小春和小丽故事里聪明的楼妈


这就是当年的并发图,震撼人心的并发图。我们可以看到蓝色的线是epoll,性能几乎不受连接数的影响(dead connections),无敌的并发!



1.2 IO多路复用(事件驱动)依旧没解决的问题

在python中,我们能很容易的写出基于事件循环的代码。

  • 红色的线:会阻塞,进行监听。当有连接来的时候,这个函数就会返回一个events,然后就会进入到蓝色的调用
  • 蓝色的调用:如果蓝色的调用,是一个非常快速的函数,那么服务器在这一步就不会阻塞,而是直接回到红色的线那里监听。然而我们知道,func函数就是基本上100%会卡住或者慢!


为什么func函数会卡住呢?原因很简单

往上看代码,这一条调用其实是会阻塞的,conn.recv是一个「读」函数,在系统同步调用中,一定是等数据全部读完了才会返回,不然就会一直卡在这里,但是这种卡,并不是cpu爆炸,而是「傻等」。


那么有什么办法可以解决呢?


1.3 事件驱动配合多线程/多进程


因为处理数据的函数,几乎100%会卡住,只要一卡住,服务器就不能再接收任何信息,只能傻等。于是,我们就可以使用多线程/多进程来执行这个处理数据的函数


那么核心思想就是:

一个线程(主线程)负责监听和分配需要处理的文件描述符,其他的线程去处理实际的数据逻辑,相互不干扰,从而实现了高并发。


对应傻逼版的故事就是:楼妈(主线程)负责告诉来客(连接),宿舍楼里有谁谁谁,在哪哪哪,然后来客就自己去找宿舍里面的人搅基,楼妈不再管了。


1.4事件驱动配合多线程/多进程都无法解决的问题


或许现在机器性能非常牛x,内存动不动8/16G,轻松几万连接怎么都能搞定了。(没错,确实是这样。


让我们回忆一下刚刚的事件驱动非阻塞模型,我们多线程\多进程主要解决的问题就是在连接到来之后,需要并发的去处理func函数,func函数会有几个原因导致他不能一直进行下去:

  1. 等待客户端发消息来:你完全不知道客户端什么时候发数据,发完数据,所以线程得等
  2. 数据库读取:数据库保存在硬盘之上,硬盘再快也比内存慢得飞起来,因此从硬盘读数据出来,就会要等
  3. func函数本身就是要去调用另外一个服务器上的数据库/api等等一系列的连环操作,才能获得数据

以上这个几个事情,都会卡住我们,并不是因为逻辑运行得多慢,cpu有多高,而是因为「IO设备太辣鸡,太缓慢了,我们必须傻等


这时候有人就说了,我们提高IO设备的速度不行吗?比如,硬盘我换成固态硬盘之类之类的,是一个解决之道。


不过,重点其实还不是这些,重点就是线程\进程太重了,每并发一个线程\进程都要消耗内存,加上线程/进程之间的切换,消耗了巨大的性能。


二)协程

协程的概念是相对多进程或者多线程来说的,他是一种协作式的用户态线程

  1. 与之相对的,线程和进程是以抢占式执行的,意思就是系统帮我们自动快速切换线程和进程来让我们感觉同步运行的感觉,这个切换动作由系统自动完成
  2. 协作式执行说的就是,想要切换线程,你必须要用户手动来切换

协程为什么那么快原因就是因为,无需系统自动切换(系统自动切换会浪费很多的资源),而协程是我们用户手动切换,而且是在同一个栈上执行,速度就会非常快而且省资源。


但是,协程有他自己的问题:协程只能有一个进程,一个线程在跑,一旦发生IO阻塞,这个程序就会卡住。


所以我们要使用协程之前,必须要保证我们所有的IO都必须是非阻塞的。


2.1 所有IO非阻塞/异步IO

IO无非就是几种

  1. 读(硬盘的)的
  2. 写(硬盘的)的
  3. 网络请求(读和写)


所有的读和写,网络请求接口都要设置成非阻塞式的,当系统内核把这些玩意儿执行完毕以后,再通过回调函数,通知用户处理。在用户空间上来看,我们就一直保持在一个线程,一个进程之中,因此,这种速率极高!


为什么Nodejs并发量会那么变态的原因就是这一点:nodejs因为js线程只有一条,为了让程序不会卡住,就一定要把所有的IO变成非阻塞异步,通过回调来告诉用户。


那么问题就来了:

  1. 我们进行IO操作的目的是因为我们想获得某些数据然后再开始进行操作,所以我们不得不在回调函数中操作。
  2. 我们在回调函数中获取到我们想要的数据,我们依靠这段数据又他妈的想要获取另外一段数据,怎么办?继续回调。
  3. 一两层你可能就觉得不行了,45678层呢?


2.2 协程真正目的


协程的真正目的其实并不是为了解决高并发而存在的,而是为了解决这种蛋痛的无限回调而存在的(其实nodejs的回调本身也没有用到多线程)


使用协程之后,我们就可以像使用「同步」的方法去写异步的调用,刚刚开始听到的时候,你可能会懵逼了。


不过没事,等我们写代码的时候就会深刻体会了。



总结:


本文属于一些笔记,也是我最近学习协程,并且复习旧概念的一些文字。在接下去的web时代里,异步io配合协程一定就是主导。


为了检验自己的学习,我使用python的asyncio+uvloop实现了一个web框架,当然是在看过了Sanic和flask源码之后,进行了一番造轮子,目前代码非常简单易读,下一篇文章,我们将看看如何使用协程进行编程。


项目地址:python类Flask框架,Luya

文档:文档

发布于 2017-11-26

文章被以下专栏收录

    这是一个新手的专栏!至于为什么是哑铃呢?因为我还是一个健身减脂小能手,我的梦想是做最强壮的程序员。