优雅的使用 Python 协程

协程(Coroutine)是由程序员在代码中显示调度的。(进程和线程是由操作系统调度,线程是操作系统调度的最小单位)。

看过前边的文章应该知道,线程任务在 IO 阻塞之后,操作系统会进行线程切换,这个切换会浪费时间与资源。而协程是在单个线程中并发的多个任务,当执行中的协程任务遇到阻塞之后,立即切换到其他就绪状态协程任务去执行,这样会极大的减小了线程因为阻塞而有操作系统切换。协程的切换属于程序级别,非常迅速,上线文完全有程序员在代码中显示管理,切换速度极快,所以减少了线程切换浪费资源。

协程的优点:

  • 协程的切换开销小,更轻量级
  • 因为 GIL 的存在,单个线程内就实现并发

协程的缺点:

  • 无法利用多个 CPU(所以在实际使用中一般是多进程 + 协程)

Python 中的 yield

yield 将一个函数构造为生成器。使用 yield 可以做协程的简单实现。当在任务中调用生成器的时,当前执行任务会切换到生成器来获取数据,获取到数据之后再切回之前的执行任务。

和 yield 相关的有两个常用方法:

  • next():执行函数找到 下一个 yield
  • send():向 yield 发送数据,然后运行代码找到下一个 yield
def consumer():
    while True:
        print('马上执行到yield')
        x = yield
        print('处理了数据:', x)


if __name__ == "__main__":
    c = consumer()
    next(c) # 第一次执行到 yield
    next(c) # 执行 yield 之后,直到下一次遇到 yield
    c.send(1) # 向 yield 发送1,向后执行直到下一次遇到 yield
    c.send(2) # 向 yield 发送2,向后执行直到下一次遇到 yield

运行结果:

马上执行到yield

处理了数据: None

马上执行到yield

处理了数据: 1

马上执行到yield

处理了数据: 2

马上执行到yield

当然出了以上的用法 还有 yield 正常一点的生成器用法:

def consumer():
    for i in range(6):
        yield i


if __name__ == "__main__":
    for i in consumer():
        print(i**2)
    
    # 或者使用 next
    # 生成器不可以二次使用
    c = consumer() 
    print(next(c))
    print(next(c))
    print(next(c))
    # 生成器中已经使用完毕,下一个报错了
    print(next(c))

运行结果:

0

1

4

0

1

2

Traceback (most recent call last):

File “markdown/test.py”, line 20, in

print(next©)

StopIteration

greenlet

greenlet 提供了 python 中协程实现的一种。最外层是调用的初始任务,内层任务是各个 greenlet。可以创建协程并在它们之间跳转。

在 greenlet 协程中跳转都是显式的:一个 greenlet(叫它g1)选择跳转到另一个 greenlet(叫它g2),这将导致 g1 暂停,而 g2 开始执行一直到遇到切换或执行完毕,当执行完毕时会自动切换到 g1,g1 继续执行(切换走的时候又状态保存)。

在 greenlet 中切换到协程需要使用 switch 方法,当对 greenlet 第一次执行 greenlet.switch() 时,需要传递参数,在后续的 switch 中不用传参(传了也无效)

from greenlet import greenlet

# 协程切换之 吃完睡 睡醒吃 吃完再睡 

def eat(user):
    print('%s eat apple' % user)
    g2.switch('Dolphin')  # 3
    print('%s eat banane' % user)
    g2.switch()


def sleep(user):
    print('%s is sleeping 1' % user)
    g1.switch("Jary")
    print('%s is sleeping 2' % user)


if __name__ == "__main__":
    g1 = greenlet(eat)
    g2 = greenlet(sleep)

    g1.switch('Dolphin')

输出结果:

Dolphin eat apple

Dolphin is sleeping 1

Dolphin eat banane

Dolphin is sleeping 2

greenlet 相比较 generator 提供了 switch 更方便切换协程,但是所有的协程都由我们手动调度还是不够友好。如果能在协程之间遇到阻塞自动调度,那可就太好了~

于是,Gevent 乘着七彩祥云来了~

Gevent

Gevent 是一个包装了 greenlet 的第三方库,他使用起来更像 threading 那样可以协程也实现自动调度,可以轻松通过 gevent 实现并发同步或异步编程。

使用 gevent.spawn 启动协程不同于 greenlet 的手动切换。使用 gevent.spawn 创建的协程,会进入就绪状态等待执行。

对于已经常见的协程,可能主线程已经结束其还未执行完毕,可以在主线程中使用 join 等待期执行完毕,或者使用 gevent.joinall() 等待多个协程 。

在 gevent 协程中,默认只认 gevent 定义的阻塞(例如:gevent.sleep())。像 time.sleep()、open()、网络请求这种阻塞会被忽略。可以通过打补丁来解决:

from gevent import monkey

# 在其之后的 time.sleep()、open()、网络请求 都被 gevent 认为会阻塞
monkey.patch_all()

# 请把这种方式当成一种习惯
from gevent import monkey;monkey.patch_all()

https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html

你可能感兴趣的:(python)