8.8 协程

我们都知道线程间的任务切换是由操作系统来控制的,而协程的出现,就是为了减少操作系统的开销,由协程来自己控制任务的切换

协程本质上就是线程。既然能够切换任务,所以线程有两个最基本的功能:一是保存状态;二是任务切换

8.8.1 协程的特点

【优点】

  • 线程任务切换开销小,属于程序级的切换,操作系统感知不到
  • 单线程内就可以实现并发的效果,最大限度的利用CPU

【缺点】

  • 协程的本质是单线程,无法利用多核;可以一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  • 协程是单个线程执行多个任务,一旦协程遇到阻塞,将会阻塞整个线程

【特点】

  • 必须要在单线程中实现并发
  • 修改共享数据不需要加锁
  • 用户程序内保存多个控制流的上下文栈
  • 一个协程遇到IO操作自动切换到其它协程
8.8.2 Greenlet

使用grennlet第三方库实现任务间的切换

from greenlet import greenlet

def get_money(name):
    print(f"{name} get 10 $")
    g2.switch('jiawen')
    print(f"{name} get 20 $")
    g2.switch()

def buy_goods(name):
    print(f"{name} buy no.1 good ")
    g1.switch()
    print(f"{name} buy no.2 good ")
    g1.switch()

if __name__ == '__main__':
    g1 = greenlet(get_money)
    g2 = greenlet(buy_goods)

    g1.switch('gailun')  # switch在第一次时必须要传入参数,以后就不需要了

效率对比

from greenlet import greenlet
import time

def f1():
    re = 1
    for i in range(10000000):
        re *= i
        g2.switch()

def f2():
    re = 1
    for i in range(10000000):
        re += i
        g1.switch()


if __name__ == '__main__':
    start = time.time()
    g1 = greenlet(f1)
    g2 = greenlet(f2)

    g1.switch()  # switch在第一次时必须要传入参数,以后就不需要了
    print(f'{time.time()- start}') # 5.822627305984497

import time

def f1():
    re = 1
    for i in range(10000000):
        re *= i


def f2():
    re = 1
    for i in range(10000000):
        re += i

start = time.time()
f1()
f2()
print(f'{time.time()- start}')  # 1.041489601135254

【结论】单纯的切换,在没有IO阻塞的情况下,协程的效率反而降低

8.8.3 Gevent介绍

Gevent也是一个第三方库,主要用来实现并发同步或是异步编程,在gevent中用到的主要模式是Greenlet, Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

【方法】

gevent.spawn(func,*args,**kwargs) spawn括号内第一个参数是函数名,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数func,spawn是异步提交任务

join() 等待调用者结束

value() 拿到调用者的返回值

遇到IO阻塞会自动切换

import gevent


def get_money(name):
    print(f"{name} get 10 $")
    gevent.sleep(2)   #模拟的是gevent可以识别的IO阻塞
    print(f"{name} get 20 $")


def buy_goods(name):
    print(f"{name} buy no.1 good ")
    gevent.sleep(1)
    print(f"{name} buy no.2 good ")


if __name__ == '__main__':
    g1 = gevent.spawn(get_money,'gailun')
    g2 = gevent.spawn(buy_goods,name='jiawen')
    g1.join()
    g2.join()
    # gevent.joinall([g1,g2])
    print('__main__')
    # 输出
gailun get 10 $
jiawen buy no.1 good 
jiawen buy no.2 good 
gailun get 20 $
__main__

【注意】如果要使gevent识别所有的io阻塞,放到被打补丁者的前面或者直接写在在文件的最开头写上以下代码

rom gevent import monkey;monkey.patch_all()

应用

# 爬虫
from gevent import monkey;monkey.patch_all()
import gevent
import requests
import time

def get_inf(url):
    print(f'GET:{url}')
    res = requests.get(url)
    if res.status_code == 200:
        print(f"{len(res.text)} get from {url}")

if __name__ == '__main__':
    start_time = time.time()
    gevent.joinall(
        [gevent.spawn(get_inf,'https://www.python.org/'),
    gevent.spawn(get_inf,'https://www.yahoo.com/'),
    gevent.spawn(get_inf,'https://github.com/'),
        ]
    )
    print(f"take {time.time()-start_time} secondes")