08协程

一、单线程下实现并发,使用yield
并发 = 切换 + 保存状态
1、yield可以保存任务运行状态,与操作系统的保存状态很像,但是yield是代码级别控制的,更轻量级
2、send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换

单纯的切换反而会降低运行效率
# 串行执行
import time
def consumer(res):
    '''任务1:接受数据,处理数据'''
    pass

def producer():
    '''任务2:生产数据'''
    res = []
    for i in range(10000000):
        res.append(i)
    return res

start_time = time.time()
res = producer()
consumer(res)
stop_time = time.time()
print('spend time:',stop_time - start_time)#spend time: 1.5560393333435059


# 基于yield并发执行
import time
def consumer():
    '''任务1:接受数据,处理数据'''
    while True:
        # print('consumer...')
        x = yield

def producer():
    '''任务2:生产数据'''
    g = consumer()
    next(g)
    for i in range(10000000):
        # print('producer...')
        g.send(i)

start_time = time.time()
# 基于yield保存状态,实现两个任务直接来回切换,即并发的效果
producer()
stop_time = time.time()
print('spend time:',stop_time - start_time)#spend time: 2.0483670234680176,效率低

 二、协程

使用yield实现的切换并不能检测到I/O阻塞,那么这样的切换并没有意义
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的,非操作系统控制切换。
解释:
1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率

优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程、

总结协程特点:
1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
三、greenlet

如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦
(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),
而使用greenlet模块可以非常简单地实现这20个任务直接的切换,
但是也无法检测I/O
from greenlet import greenlet

def eat(name):
    print('{} eat 1'.format(name))
    g2.switch('cc')
    print('{} eat 2'.format(name))
    g2.switch()

def play(name):
    print('{} play 1'.format(name))
    g1.switch()
    print('{} play 2'.format(name))

g1 = greenlet(eat)#不用传参数
g2 = greenlet(play)

g1.switch('cc')#在第一次switch时传入参数,以后不需要
四、gevent
import gevent
def eat(name):
    print('{} eat 1'.format(name))
    gevent.sleep(3) # 遇到I/O阻塞会自动切换任务,但这里是gevent.sleep(3)模拟的识别的阻塞
    print('{} eat 2'.format(name))

def play(name):
    print('{} play 1'.format(name))
    gevent.sleep(1)
    print('{} play 2'.format(name))

g1 = gevent.spawn(eat, 'cc')
g2 = gevent.spawn(play, 'dd')

g1.join()
g2.join()
# 或者gevent.joinall([g1,g2])
上例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 gevent
import time

def eat(name):
    print('{} eat 1'.format(name))
    time.sleep(3)  # 遇到I/O阻塞会自动切换任务,但这里是gevent.sleep(3)模拟的识别的阻塞
    print('{} eat 2'.format(name))

def play(name):
    print('{} play 1'.format(name))
    time.sleep(1)
    print('{} play 2'.format(name))

g1 = gevent.spawn(eat, 'cc')
g2 = gevent.spawn(play, 'dd')

g1.join()
g2.join()

 

例1:

 

posted @ 2021-03-05 17:08  cheng4632  阅读(63)  评论(0编辑  收藏  举报