Skip to content

协程

协程,时间管理大师

Note

线程 是系统级别的,由操作系统调度

协程 是程序级别的,由程序根据需要自己调度

协程可以想象成线程里的线程,也就是将一个线程里多个任务分拆给多个协程。

使用线程的时候是:线程执行遇到耗时(阻塞)的地方,再开一个线程,去执行这个耗时的任务,原本的线程继续执行。 就好比我在算题,算着算着计算量比较大比较耗时,我就叫了小明帮我算,我继续算下面的。

使用协程的时候是:利用别的协程执行耗时操作时的那点时间切换去别的协程继续执行,也就是在协程之间反复横跳。 就好比我在算题,算着算着计算量有点大,就先做别的题,回头自己再继续把题算完

协程的优点: 1. 无需线程上下文切换的开销,因为都在同一个线程里,避免了无意义的调度,由此可以提高性能。 2. 无需原子操作锁定及同步的开销 3. 方便切换控制流,简化编程模型 4. 高并发+高扩展性+低成本:一个CPU支持上万个协程都不是问题。所以很适合高并发处理。

缺点: 1. 因为协程都是程序调度的,所以需要程序员承担调度的责任,同时也失去了标准线程使用多CPU的能力 2. 无法利用多核资源:协程的本质是个单线程,不能同时用上单个CPU的多个核。协程需要和进程配合才能运行在多CPU上,不过绝大部分应用都没有这个必要,除非是CPU密集型应用 3. 进行阻塞操作(如IO)会阻塞掉整个程序。

使用 yield 实现并发

import time 

def task_1():
    while True:
        print("{:-^16s}".format("task_1"))
        time.sleep(0.1)
        yield

def task_2():
    while True:
        print("{:-^16s}".format("task_2"))
        time.sleep(0.1)
        yield

def main():
    t1 = task_1()
    t2 = task_2()
    # 先让t1运行一会儿,当t1中遇到yield的时候,再返回到main()的while循环
    # 然后执行t2,当它遇到yield的时候,再次切换到t1中
    # 就这样t1/t2/t1/t2...交替运行,最终实现了多任务...协程
    while True:
        next(t1)
        next(t2)


if __name__ == '__main__':
    main()

--------------------------------------------------

# Output:
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
...

使用 greenlet 实现并发

>_: pip install greenlet

from greenlet import greenlet
import time

def task_1():
    while True:
        print("{:-^16s}".format("task_1"))
        gr2.switch()        # 切换到 gr2中,也就是 task_2() 中运行
        time.sleep(0.5)


def task_2():
    while True:
        print("{:-^16s}".format("task_2"))
        gr1.switch()        # 切换到 gr1中,也就是 task_1() 中运行
        time.sleep(0.5)

gr1 = greenlet(task_1)
gr2 = greenlet(task_2)

# 切换到 gr1中,也就是 task_1() 中运行
gr1.switch

--------------------------------------------------

# Output:
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
-----task_1-----
-----task_2-----
...

使用 gevent 实现并发

import gevent
gevent.spawn(funcName, args)

先看看没有阻塞操作的时候:

# 没有阻塞
import gevent


def task_1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)


def task_2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)


def task_3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)


def main():
    g1 = gevent.spawn(task_1, 5)
    g2 = gevent.spawn(task_2, 5)
    g3 = gevent.spawn(task_3, 5)

    g1.join()
    g2.join()
    g3.join()


if __name__ == '__main__':
    main()

--------------------------------------------------

# Output:
<Greenlet at 0x1f6eaf0cd00: task_1(5)> 0
<Greenlet at 0x1f6eaf0cd00: task_1(5)> 1
<Greenlet at 0x1f6eaf0cd00: task_1(5)> 2
<Greenlet at 0x1f6eaf0cd00: task_1(5)> 3
<Greenlet at 0x1f6eaf0cd00: task_1(5)> 4
<Greenlet at 0x1f6eaf0ce10: task_2(5)> 0
<Greenlet at 0x1f6eaf0ce10: task_2(5)> 1
<Greenlet at 0x1f6eaf0ce10: task_2(5)> 2
<Greenlet at 0x1f6eaf0ce10: task_2(5)> 3
<Greenlet at 0x1f6eaf0ce10: task_2(5)> 4
<Greenlet at 0x1f6eaf0cbf0: task_3(5)> 0
<Greenlet at 0x1f6eaf0cbf0: task_3(5)> 1
<Greenlet at 0x1f6eaf0cbf0: task_3(5)> 2
<Greenlet at 0x1f6eaf0cbf0: task_3(5)> 3
<Greenlet at 0x1f6eaf0cbf0: task_3(5)> 4
gevent 是利用协程阻塞的时候去执行别的协程 这里没有阻塞操作,所以并不会发生什么变化,跟普通函数调用一样,更提不上并发

现在来加点阻塞操作看看

# 有阻塞
import gevent


def task_1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        # time.sleep(0.1)    # 在gevent中,time.sleep()这种阻塞操作是不起作用的
        gevent.sleep(0.1)


def task_2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(0.1)


def task_3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(0.1)


def main():
    g1 = gevent.spawn(task_1, 5)
    g2 = gevent.spawn(task_2, 5)
    g3 = gevent.spawn(task_3, 5)

    g1.join()
    g2.join()
    g3.join()



if __name__ == '__main__':
    main()

--------------------------------------------------

# Output:
<Greenlet at 0x1433a62b370: task_1(5)> 0
<Greenlet at 0x1433a62b590: task_2(5)> 0
<Greenlet at 0x1433a62b480: task_3(5)> 0
<Greenlet at 0x1433a62b370: task_1(5)> 1
<Greenlet at 0x1433a62b590: task_2(5)> 1
<Greenlet at 0x1433a62b480: task_3(5)> 1
<Greenlet at 0x1433a62b370: task_1(5)> 2
<Greenlet at 0x1433a62b590: task_2(5)> 2
<Greenlet at 0x1433a62b480: task_3(5)> 2
<Greenlet at 0x1433a62b370: task_1(5)> 3
<Greenlet at 0x1433a62b590: task_2(5)> 3
<Greenlet at 0x1433a62b480: task_3(5)> 3
<Greenlet at 0x1433a62b370: task_1(5)> 4
<Greenlet at 0x1433a62b590: task_2(5)> 4
<Greenlet at 0x1433a62b480: task_3(5)> 4

上述代码 建立了三个 gevent 对象,里面分别转载了 task_1、task_2、task_3 三个执行函数。 接着三个 gevent 对象都使用 join 方法运行了执行函数 在三个执行函数中又用了 gevent.sleep() 模拟阻塞操作(在实际开发中并不会专门用sleep去阻塞,而是在执行到 IO 等耗时操作时,gevent自动切换。)

在执行 task_1 的时候,打印了第一句,然后遇到了 gevent.sleep() 的阻塞操作,切换到 task_2 ,打印了第二局,又遇到阻塞,又切换...一直到全部执行完毕

另外,task_1 中的 time.sleep() 在 gevent 管理的协程中是不起作用的,需要使用 gevent.sleep() 才行。

这就带来一个问题:如果我的代码是开发完了才加入了 gevent ,那岂不是要把很多地方手动改到 gevent 能接受。还好 gevent 提供了一个补丁

gevent 补丁

import time
import gevent
from gevent import monkey

monkey.patch_all()    # 打补丁, 一定要写在最上方


def task_1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def task_2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def task_3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def main():
    g1 = gevent.spawn(task_1, 5)
    g2 = gevent.spawn(task_2, 5)
    g3 = gevent.spawn(task_3, 5)

    g1.join()
    g2.join()
    g3.join()


if __name__ == '__main__':
    main()

--------------------------------------------------

# Output:
<Greenlet at 0x1433a62b370: task_1(5)> 0
<Greenlet at 0x1433a62b590: task_2(5)> 0
<Greenlet at 0x1433a62b480: task_3(5)> 0
<Greenlet at 0x1433a62b370: task_1(5)> 1
<Greenlet at 0x1433a62b590: task_2(5)> 1
<Greenlet at 0x1433a62b480: task_3(5)> 1
<Greenlet at 0x1433a62b370: task_1(5)> 2
<Greenlet at 0x1433a62b590: task_2(5)> 2
<Greenlet at 0x1433a62b480: task_3(5)> 2
<Greenlet at 0x1433a62b370: task_1(5)> 3
<Greenlet at 0x1433a62b590: task_2(5)> 3
<Greenlet at 0x1433a62b480: task_3(5)> 3
<Greenlet at 0x1433a62b370: task_1(5)> 4
<Greenlet at 0x1433a62b590: task_2(5)> 4
<Greenlet at 0x1433a62b480: task_3(5)> 4

joinall

有没有发现

g1.join() g2.join() g3.join()

写了三个join, 不如给他来个一次性:

gevent.joinall(spawn_list, timeout=None, raise_error=False, count=None)

import time
import gevent
from gevent import monkey

monkey.patch_all()


def task_1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def task_2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def task_3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.1)


def main():
    # joinall 接受一个 spawn 列表
    gevent.joinall([
        gevent.spawn(task_1, 5),
        gevent.spawn(task_2, 5),
        gevent.spawn(task_3, 5)
    ] timeout=5)


if __name__ == '__main__':
    main()

--------------------------------------------------

# Output:
<Greenlet at 0x12e8cba9d00: task_1(5)> 0
<Greenlet at 0x12e8cba9e10: task_2(5)> 0
<Greenlet at 0x12e8cba9bf0: task_3(5)> 0
<Greenlet at 0x12e8cba9d00: task_1(5)> 1
<Greenlet at 0x12e8cba9e10: task_2(5)> 1
<Greenlet at 0x12e8cba9bf0: task_3(5)> 1
<Greenlet at 0x12e8cba9d00: task_1(5)> 2
<Greenlet at 0x12e8cba9e10: task_2(5)> 2
<Greenlet at 0x12e8cba9bf0: task_3(5)> 2
<Greenlet at 0x12e8cba9d00: task_1(5)> 3
<Greenlet at 0x12e8cba9e10: task_2(5)> 3
<Greenlet at 0x12e8cba9bf0: task_3(5)> 3
<Greenlet at 0x12e8cba9d00: task_1(5)> 4
<Greenlet at 0x12e8cba9e10: task_2(5)> 4
<Greenlet at 0x12e8cba9bf0: task_3(5)> 4

只管在 joinall 里创建 spawn 对象就行,其他不用管,gevent 自己会处理好的 这也是最常用的方法