协程


什么是协程

  1. 在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位。
  2. 无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
  3. 基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。
  • 并发的本质:切换+保存状态
    cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。

  • 协程的本质
    就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

  1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
  2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

实现在多个任务之间切换 yield + send

  • yield无法做到遇到io阻塞
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    # 进程 : 启动多个进程,进程之间是由操作系统负责调用
    # 线程 : 启动多个线程,真正被CPU执行的最小单位实际是线程
    # 开启一个线程 :创建1个线程,属于线程的内存开销 寄存器和堆栈
    # 关闭一个线程
    # 线程在CPython下,由于全局GLI锁,多线程没有办法同时访问CPU,真正工作只有一个CPU

    # 协程 : 本质上是一个线程
    # 在多个任务之间切换,来节省IO时间
    # 无需切换寄存器 和 堆栈,只是正常在程序之间切换
    # ***** 能在多个任务之间切换
    # ***** 协程中任务之间的切换也消耗时间,但是开销,要远远小于进程线程之间的切换。


    # 实现并发的手段

    ### 实现在多个任务之间切换 yield + send
    import time
    def consumer():
    # print(111111)
    while True:
    x = yield
    time.sleep(1)
    print('处理了数据: ' ,x)

    def producer():
    c = consumer() # 生成器
    next(c) # 激活生成器,send之前必须next
    for i in range(10):
    time.sleep(1)
    print('生产了数据 %s ' %i)
    c.send(i)

    producer() # 在producer控制了consumer函数,并且来回切换 即-在两个任务中切换

    c = consumer() # 生成器
    print(c) # <generator object consumer at 0x00000000021C84C0>
    c.__next__()
    next(c)
    c.send(100)
    c.send(200)

greenlet 模块切换执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from greenlet import greenlet
# pip3 install greenlet
# pip3 install gevent

# greenlet 学习一下 后续不用
# 协程模块
# 切换执行程序
# 真真干的协程模块,就是使用greenlet完成的切换

def eat():
print('eating start')
g2.switch() # 第1次切换 在这里switch(切换),不仅切换还会记录执行的位置
print('eating end')
g2.switch()

def play():
print('playing start')
g1.switch()
print('playing end')

# 把执行的方法 交给greenlet
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch() # 首先让g1执行

# 结果: 出现switch即切换程序
# eating start
# playing start
# eating end
# laying end

# 进程 4c+1 = 5
# 线程 4c*5 = 20
# 协程 每个线程里最多可以启动500个协程
# 并发 1个进程20个线程 5个进程 100个线程 100个线程 = 50000W个协程
# nginx 分发任务,最大承载量 5W
# 在IO等待的时候 切换执行其他任务

使用gevent模块实现协程

  • 安装:pip3 install gevent

  • Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

1
2
3
4
5
6
# g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
# g2=gevent.spawn(func2)
# g1.join() #等待g1结束
# g2.join() #等待g2结束
# 或者上述两步合作一步:gevent.joinall([g1,g2])
# g1.value#拿到func1的返回值

使用gevent实现IO切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from gevent import monkey;monkey.patch_all()
# 会把后面所有的阻塞操作都打成包,就会认识到time.sleep
import time
import gevent
import threading

# 遇到IO切换
def eat():
print(threading.current_thread().getName()) # DummyThread-1 仿制品线程 虚拟线程 -- 协程
print('eating start')
time.sleep(1)
# gevent.sleep(1)
print('eating end')

def play():
print(threading.current_thread().getName())
print('playing start')
time.sleep(1)
# gevent.sleep(1)
print('playing end')

g1 = gevent.spawn(eat) # gevent 遇到IO会自动切换
g2 = gevent.spawn(play)
g1.join() # 等待g1执行结束
g2.join() # 等待g2执行结束,不然则异步
# 或者上述两步合作一步:gevent.joinall([g1,g2])


# 结果
# eating start
# playing start 同时开始
# 同时睡一秒
# eating end 同时结束
# playing end

# 总结:
# 1、进程和线程的任务切换 由操作系统完成,而协程任务切换由程序代码完成,只有遇到协程模块能识别的IO操作的时候,程序才会执行任务切换
# 实现并发效果。
# 2、协程是通过greenlet模块的switch()方法控制切换

gevent的同步和异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,
# gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
# from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
# 或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

from gevent import monkey;monkey.patch_all()
import time
import gevent

def task():
time.sleep(1) # 模拟IO阻塞
print(12345)

def sync(): # synchronous = sync 同步
for i in range(10):
task()

def async(): # 异步
g_lst = []
for i in range(10):
g = gevent.spawn(task)
g_lst.append(g)
# for g in g_lst:g.join() # 等待所有的协程结束
gevent.joinall(g_lst) # 接收可迭代对象 每个g都执行完join

# sync() # 同步 睡一秒执行一个
async() # 异步 都睡一秒一起执行

# 协程 适合用于网络操作,比如爬虫

协程爬虫小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 协程:能够在线程中实现并发效果
# 能够规避一些在任务中遇到的IO操作
# 在任务的执行过程中,检测到IO就切换到其他任务

### 协程 — 爬虫例子
# 正则基础
# 请求过程中的IO等待

from gevent import monkey;monkey.patch_all()
import requests
import gevent
url = 'https://maoyan.com/board/4'

# res = requests.get(url)
# print(len(res.content.decode('utf-8')))
# print(len(res.text))


def get_url(url):
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/71.0.3578.98 Safari/537.36'
}
res = requests.get(url,headers=headers)
html = res.content.decode('utf-8')
return len(html)

# ret = get_url('http://www.baidu.com')

g1 = gevent.spawn(get_url,'http://www.tianmao.com')
g2 = gevent.spawn(get_url,'http://www.taobao.com')
g3 = gevent.spawn(get_url,'http://www.jd.com')
g4 = gevent.spawn(get_url,'https://maoyan.com/board/4')

gevent.joinall([g1,g2,g3,g4])
print(g1.value)
print(g2.value)
print(g3.value)
print(g4.value)

# 总结:
# 为什么用到协程
# 多线程在CPython解释器下,存在GIL锁,无法真正的使用多个CPU,多个线程之间的切换就会浪费时间
# 可以把一个线程的作用发挥到极致 1个线程可以开启500个协程,提高了CPU的利用率
# 协程相比于多线程的优势 ,切换的效率更快