由于GIL的存在,导致Python多线程性能甚至比单线程更糟。

协程: 协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.

协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。

  1. 写一个·简单的协程
def run1():
    print('run1---task1')
    yield
    print('run1---task2')
    yield
    print('run1---task3')
    yield
    print('run1---task4')
    yield
    print('run1---task5')
    yield
    
def run2():
    print('run2---task1')
    yield
    print('run2---task2')
    yield
    print('run2---task3')
    yield
    print('run2---task4')
    yield
    print('run2---task5')
    yield
    
def main():
    print('main---task')
    r1 = run1()
    r2 = run2()
    while True:
        try:
            next(r1)
            next(r2)
        except Exception:
            print('所有任务运行完毕')
            break
            
if __name__ == '__main__':
    main()

协程,其实就是伪多线程,利用生成器yield会暂停一下的原理,一个函数执行一块,另一个函数再执行,到yield关键字就暂停换别的函数执行,这和多线程是很相似的。

  1. 协程执行过程
    协程中yield关键字通常出现在等号的右边,比如c = yield a + b。在赋值语句中,等号右边的代码在赋值之前执行。因此,协程首先会执行yield a + b,产出表达式a + b的值,然后协程会在yield关键字所在的位置暂停(suspend)。等到调用方执行.send(10)时,协程会从之前暂停的地方恢复执行(resume),而且.send(10) 方法的参数10会成为暂停的yield表达式的值。所以yield a + b整体等于10,然后再赋值给变量c:
def run(a):
    print(a)
    b = yield a
    print(b)
    c = yield b
    print(c)
    yield c
    
r = run(1)
next(r) #预激协程
r.send(2)
r.send(3)

# 输出结果

# 1
# 2
# 3
  1. 预激协程的装饰器
import functools

def active(func):
    @functools.wraps(func)
    def real_run(*args,**kwargs):
        r = func(*args,**kwargs)
        next(r)
        return r
    return real_run
        
    
    
@active
def run(a):
    print(a)
    b = yield a
    print(b)
    c = yield b
    print(c)
    yield c
    

r = run(1)
r.send(2)
r.send(3)

# 输出结果

# 1
# 2
# 3
  1. close()和throw()方法是用来终止协程的,由于协程遇到异常就会终止,所以我们终止协程的方法就是向协程发送异常,但是要保证这个异常只会让协程结束,而不会让全部程序结束
  1. 所以这暗示了一种终止协程的方法:发送某种错误,让协程退出,
    generator.throw(exc_type[, exc_value[, traceback]])
    如果协程中没有处理异常的语句,则异常往上冒泡
  2. 另一种终止协程则是抛出GeneratorExit异常,即:
    generator.close()

throw方法向协程抛出异常

def run():
    a = 0
    while True:
        try:
            a = yield a
            print(a)
        except TypeError:
            print('出现异常')
    
r = run()
next(r) #预激协程
r.send(1)
r.throw(TypeError,'throw a erro')#向协程中抛出异常
r.send(2)
r.send(3)

# 输出结果

# 1
# 出现异常
# 2
# 3

向协程中抛出异常。并在协程中处理,若传入协程中的异常没有处理,那么协程就会关闭。

用close关闭协程之后,对协程的任何操作,例如send(),next()都将失效,若强制使用,就会报错

def run():
    a = 0
    while True:
        try:
            a = yield a
            print(a)
        except TypeError:
            print('出现异常')
    
r = run()
next(r) #预激协程
r.send(1)
r.close()#关闭协程
r.send(2)
r.send(3)

# 输出结果

# 1
# 报错StopIteration
  1. 让协程返回值
def run(a):
    print(a)
    b = yield a
    print(b)
    c = yield b
    print(c)
    yield c
    return a + b + c
    
r = run(1)
next(r) #预激协程
r.send(2)
r.send(3)
result = r.send(None)
print('reuslt = ',result)

# 输出结果
# 1
# 2
# 3
# 报错StopIteration: 6
  • 从结果我们可以看到,返回值在异常里,接下来我们从异常中获取这个值,只需要捕获这个异常,获得其中的值
try:
    result = r.send(None)
except StopIteration as exc:
    print('reuslt = ',exc.value)

# 输出结果
# 1
# 2
# 3
# 报错StopIteration: 6
  1. 生产者消费者模式(生产一个,消费一个)
import random
import time

def producer(a):
    next(a)
    while True:
        item = random.randint(0,99)
        print('生产者生产产品',item)
        time.sleep(1)
        a.send(item)
        
def consumer():
    while True:
        product = yield
        print('消费者消费',product)
        time.sleep(1)
        
producer(consumer())

# 输出结果

# 生产者生产产品 39
# 消费者消费 39
# 生产者生产产品 16
# 消费者消费 16
# 生产者生产产品 13
# 消费者消费 13
# 生产者生产产品 51
# 消费者消费 51
# 生产者生产产品 92
# 消费者消费 92
# 生产者生产产品 31
# 消费者消费 31
  1. greenlet协程
    由来 虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。 与此同时,Python的一个衍生版 Stackless Python。实现了原生的协程,它更利于使用。 于是,大家开始将 Stackless 中关于协程的代码 。单独拿出来做成了CPython的扩展包。 这就是 greenlet 的由来,因此 greenlet 是底层实现了原生协程的 C扩展库。
import greenlet
import random
import time

def producer():
    while True:
        item = random.randint(0,99)
        print('生产者生产产品',item)
        time.sleep(1)
        c.switch(item)# 将item转给c并跳转到c
        
def consumer():
    while True:
        product = p.switch()
        print('消费者消费',product)
        time.sleep(1)

c = greenlet.greenlet(consumer)# 创建协程
p = greenlet.greenlet(producer)
c.switch()# c先执行

# 输出结果

# 生产者生产产品 23
# 消费者消费 23
# 生产者生产产品 27
# 消费者消费 27
# 生产者生产产品 37
# 消费者消费 37
# 生产者生产产品 98
# 消费者消费 98
# 生产者生产产品 47
# 消费者消费 47
# 生产者生产产品 3
# 消费者消费 3
# 生产者生产产品 10
# 消费者消费 10

但是greenlet还是要手动跳转协程,所以之后有了gevent协程

  1. gevent协程
    gevent协程主要实现了自动跳转协程,当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
import gevent

def run(n):
    for i in range(n):
        print(gevent.getcurrent(),i)
        
g1 = gevent.spawn(run,5)
g2 = gevent.spawn(run,5)
g3 = gevent.spawn(run,5)

g1.join()# 等待g1执行完毕,相当于是一个耗时的事件
g2.join()
g3.join()

# 输出结果
# <Greenlet at 0x16337dff260: run(5)> 0
# <Greenlet at 0x16337dff260: run(5)> 1
# <Greenlet at 0x16337dff260: run(5)> 2
# <Greenlet at 0x16337dff260: run(5)> 3
# <Greenlet at 0x16337dff260: run(5)> 4
# <Greenlet at 0x16337dff6a0: run(5)> 0
# <Greenlet at 0x16337dff6a0: run(5)> 1
# <Greenlet at 0x16337dff6a0: run(5)> 2
# <Greenlet at 0x16337dff6a0: run(5)> 3
# <Greenlet at 0x16337dff6a0: run(5)> 4
# <Greenlet at 0x16337dff590: run(5)> 0
# <Greenlet at 0x16337dff590: run(5)> 1
# <Greenlet at 0x16337dff590: run(5)> 2
# <Greenlet at 0x16337dff590: run(5)> 3
# <Greenlet at 0x16337dff590: run(5)> 4
# <Greenlet at 0x16337dff7b0: run(5)> 0
# <Greenlet at 0x16337dff7b0: run(5)> 1
# <Greenlet at 0x16337dff7b0: run(5)> 2
# <Greenlet at 0x16337dff7b0: run(5)> 3
# <Greenlet at 0x16337dff7b0: run(5)> 4
# <Greenlet at 0x16337dff8c0: run(5)> 0
# <Greenlet at 0x16337dff8c0: run(5)> 1
# <Greenlet at 0x16337dff8c0: run(5)> 2
# <Greenlet at 0x16337dff8c0: run(5)> 3
# <Greenlet at 0x16337dff8c0: run(5)> 4
# <Greenlet at 0x16337dff9d0: run(5)> 0
# <Greenlet at 0x16337dff9d0: run(5)> 1
# <Greenlet at 0x16337dff9d0: run(5)> 2
# <Greenlet at 0x16337dff9d0: run(5)> 3
# <Greenlet at 0x16337dff9d0: run(5)> 4

分析一下这段代码,我们用gevent.spawn()创建了三个协程,三个协程被创建的一瞬间就能够开始执行,但是并没有执行。当调用到g1.jion()表示等待g1执行结束,这是一个很耗时间的过程,所以系统就会检测我们之前创建过的协程,发现有g1协程可以执行,就开始执行g1,以此类推。
但是这个运行结果并不是我们想要的,因为它不像是并发,是单线程的,所以我们要通过修改代码来实现。

import gevent

def run(n):
    for i in range(n):
        print(gevent.getcurrent(),i)
        gevent.sleep(0.5)# 耗时语句
        
g1 = gevent.spawn(run,5)
g2 = gevent.spawn(run,5)
g3 = gevent.spawn(run,5)

g1.join()# 等待g1执行完毕,相当于是一个耗时的事件
g2.join()
g3.join()

# 输出结果

# <Greenlet at 0x1633627ce10: run(5)> 0
# <Greenlet at 0x16337dff590: run(5)> 0
# <Greenlet at 0x16337dff6a0: run(5)> 0
# <Greenlet at 0x1633627ce10: run(5)> 1
# <Greenlet at 0x16337dff590: run(5)> 1
# <Greenlet at 0x16337dff6a0: run(5)> 1
# <Greenlet at 0x1633627ce10: run(5)> 2
# <Greenlet at 0x16337dff590: run(5)> 2
# <Greenlet at 0x16337dff6a0: run(5)> 2
# <Greenlet at 0x1633627ce10: run(5)> 3
# <Greenlet at 0x16337dff590: run(5)> 3
# <Greenlet at 0x16337dff6a0: run(5)> 3
# <Greenlet at 0x1633627ce10: run(5)> 4
# <Greenlet at 0x16337dff590: run(5)> 4
# <Greenlet at 0x16337dff6a0: run(5)> 4

我们加上了一个gevent.sleep(),就能实现并发。
当g1运行到sleep()的时候,系统会寻找其它的协程,找到g2,g2也会运行的sleep(),而这个时候g1的sleep()并没有结束,所以进入到g3,之后发现没有协程可以选择,就等待g1,继续执行。

若要执行阻塞时间实现协程转换,就必须把所有的阻塞全部换成gevent的,例如gevent.sleep(),但是用time.sleep()就不行。这样就很麻烦,所以我们可以打个补丁

from gevent import monkey
monkey.patch_all()

加上这句话,我们用阻塞操作就可以不用gevent专有的了,有了这个补丁,系统自动把程序的所有阻塞事件换成gevent特有的
协程的真正意义:把延迟的时间利用起来,去做别的事情,提高效率。