理解python协程不阻塞

1. 什么是阻塞

厨房的下水道被堵了,要么自己想办法疏通,要么请物业找师傅给疏通,如若不然,一滴水都留不下去,你可以用这种类比方式来理解阻塞。

计算机领域里的阻塞,专指IO操作。

当我们说程序被阻塞了,指的是程序卡在那里不动了,不运行了。它为什么不运行呢,因为它在做IO操作,可能是在读取一个文件,也可能是在从socket里读取数据,但是还没有得到返回结果。于是乎,就傻乎乎的在那里等,我们所以为的等,专业的描述是线程被挂起。

不要以为阻塞是由于IO操作太繁忙所导致的,持这种观点,说明你可能把阻塞理解成堵车了,尽管大量的IO操作确实可以可能导致阻塞,但这只是导致阻塞情况中的一种。路上只有两辆车,前面的车停在路中间,你的车在后面,就走不了,你堵在那里,不是因为车多,而是因为前面的车不走了。

前面的车为啥不走呢,原因是司机正在给老婆打电话,而且还振振有词的说,边打电话边开车很危险,什么时候打完电话,什么时候开车。打电话这个动作,就好比是IO操作,IO不结束,线程就一直挂起,车就一直停在那。

是不是很无语,那你为啥不能把车停在路边呢,把路权交出来,让其他不打电话的司机继续开车呀!这个思路很好,用专业的词来描述就是异步IO,进行IO操作时,将程序的控制权交出去,让其他的代码可以正常运行,不至于大家都在那等你进行IO操作。等IO操作有了结果了,我们再把程序的控制权交还给你,你可以继续执行。

上面这段,提出了一个重要的概念,为了不阻塞,阻塞的操作一定要把程序的控制权交出去,该如何理解这个概念呢,我先举一个阻塞的例子,先看看不把程序控制权交出去会怎样

import time
from datetime import datetime

def func1():
    print("func1开始执行,输出当前时间:" + str(datetime.now()))
    time.sleep(3)       # 模拟IO操作
    print("func1执行结束,输出当前时间:" + str(datetime.now()))

def func2():
    print("func2执行,输出当前时间:" + str(datetime.now()))

func1()
func2()

函数func1中,sleep 了3秒钟,模拟IO操作。先执行func1, 后执行func2, 输出结果如下

func1开始执行,输出当前时间:2021-04-02 09:05:48.683618
func1执行结束,输出当前时间:2021-04-02 09:05:51.697345
func2执行,输出当前时间:2021-04-02 09:05:51.697345

从输出内容可以看得出,func1执行了3秒钟,func2只有等到func1执行结束后才能开始执行,func1就如同那辆停在路中间的车,把你的车堵的死死的,func2原本可以很快就执行完,就好比你只需要再向前开10米就到家了。

我们怎么才能让func1把程序控制权交出来呢,在等待IO操作的3秒钟时间里,允许func2正常执行,不耽误func2?这种情况下,就可以用协程。

from datetime import datetime
import gevent


def func1():
    print("func1开始执行,输出当前时间:" + str(datetime.now()))
    gevent.sleep(3)
    print("func1执行结束,输出当前时间:" + str(datetime.now()))


def func2():
    print("func2开始执行,输出当前时间:" + str(datetime.now()))
    print("func2执行结束,输出当前时间:" + str(datetime.now()))

green_1 = gevent.spawn(func1)
green_2 = gevent.spawn(func2)

green_1.join()
green_2.join()

先解释这段代码与上一段代码的不同之处

  1. 我没有用time.sleep,而是用了gevent.sleep(3),同样是sleep 3秒钟,time.sleep是阻塞式操作,而gevent.sleep是非阻塞式操作,那3秒钟相对于func1还是要等待的,但是把程序控制权交出去了
  2. 我用spawn方法先启动func1的执行,后启动func2
  3. 一定要调用join方法,否则程序直接就结束了,这个操作和线程的join是同样的功能,只有当green_1 和 green_2 都结束,整个程序才结束

来看输出结果

func1开始执行,输出当前时间:2021-04-02 09:16:12.506650
func2开始执行,输出当前时间:2021-04-02 09:16:12.506650
func2执行结束,输出当前时间:2021-04-02 09:16:12.506650
func1执行结束,输出当前时间:2021-04-02 09:16:15.518202

func1的确是先执行的,但是紧接着执行gevent.sleep(3),func1把程序控制权交出来,这样,func2获得程序控制权,也立即执行。func1在sleep期间,func2就已经执行结束了,并没有阻塞func2的执行,func1在sleep了3秒以后,重新拿会程序控制权,得以继续执行。

或许你听说过协程很快,用gevent部署Flask服务可以提供很高的性能,tornado采用异步IO也有很高的性能,看完这篇文章,你应当理解,为什么他们快,究竟快在了哪?快,就快在每当有阻塞操作时,都会将程序控制权交出来,让其他不阻塞的操作继续执行。这就是在小的交通事故处理过程中,交通部门提倡大家报警拍照保留证据后先将事故车辆移动到路边的原因,您别堵在那啊,其他的车又没有出事故。

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案