python协程

本文深度参考自廖雪峰老师的网站:https://www.liaoxuefeng.com/wiki/1016959663602400/1017968846697824

1 协程

协程(coroutine)的调用类似于子函数,即在函数A的执行过程中去执行子函数B,B执行完成后继续执行A。但使用协程实现的代码没有明显的函数调用。

协程是用同一个线程在不同的函数间切换,随时中断一个函数的执行去执行另一个函数。其执行过程类似于下述方式:

def A():
    print('1')
    print('2')
    print('3')

def B():
    print('x')
    print('y')
    print('z')

执行结果可能为:

1
x
y
2
z
3

协程是由同一个线程交叉去执行多个函数的,相比于多线程,其优点有:

  1. 协程的执行由代码自身控制,不是线程切换,不存在线程切换的开销,执行效率更高;
  2. 协程无需多线程的同步控制机制,无需加锁,不存在写变量冲突,同样执行效率更高。

协程只用到了一个线程,对于多核CPU,可以采用多进程+协程的方式,即利用了多核,又利用协程提高了执行效率。

2 通过生成器实现协程

python是通过generator实现协程的,在generator中,不但可以通过for循环进行遍历,还可以通过调用next函数返回其生成的下一个值。

yield不但可以返回下一个值,还可以接收调用者发出的参数

下面是通过协程实现的生产者-消费者代码:

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[consumer] Consuming %s ...' %n)
        r = '200 OK'

def producer(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[producer] Producing %s ...' %n)
        r = c.send(n)
        print('[producer] Consumer response %s ....' %r)
    c.close()

c = consumer()
producer(c)

执行结果:

[producer] Producing 1 ...
[consumer] Consuming 1 ...
[producer] Consumer response 200 OK ....
[producer] Producing 2 ...
[consumer] Consuming 2 ...
[producer] Consumer response 200 OK ....
[producer] Producing 3 ...
[consumer] Consuming 3 ...
[producer] Consumer response 200 OK ....
[producer] Producing 4 ...
[consumer] Consuming 4 ...
[producer] Consumer response 200 OK ....
[producer] Producing 5 ...
[consumer] Consuming 5 ...
[producer] Consumer response 200 OK ....

执行过程为:

  1. 设置consumer为生成器,将其传入producer
  2. producer中调用c.send(None),启动生成器;
  3. producer中通过循环生成n,通过c.send(n)发送到consumer,切换到consumer执行;
  4. consumer通过yield拿到输入值执行,再通过yield返回执行结果;
  5. producer执行结束,不再生成新的数据,通过c.close()关闭consumer,整个过程结束。

上述整个过程无锁,由同一个线程执行,producerconsumer协作完成任务,所以称之为“协程”,而非线程的抢占式多任务。

2 asyncio实现异步IO

python3.4引入了asyncio,内置了对异步IO的支持。asyncio的编程模型就是一个消息循环,我们从asyncio中取得一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

如下面代码所示:

import asyncio
import threading

@asyncio.coroutine
def hello():
    print('Hello World! (%s)' %threading.currentThread())
    r = yield from asyncio.sleep(1)
    print('Hello Again! (%s)' %threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

执行结果:

Hello World! (<_MainThread(MainThread, started 139742271719168)>)
Hello World! (<_MainThread(MainThread, started 139742271719168)>)
(大约暂停1)
Hello Again! (<_MainThread(MainThread, started 139742271719168)>)
Hello Again! (<_MainThread(MainThread, started 139742271719168)>)

代码关键点:

  • @asyncio.coroutine装饰符,将一个generator转换为coroutine
  • yield from获取另一个生成器,以进行指定的异步操作,这里使用asyncio.sleep()产生一个生成器函数,作用是暂停指定的时间;
  • 将多个coroutine装入列表中,使用asyncio.await将所有的任务转换为awaitable objects,对所有的任务进行并发执行;
  • asyncio.get_event_loop()获取一个EventLoop的引用,使用run_until_complete等待所有的任务执行完成。
  • asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,因为IO操作时不需要CPU,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,上面的输出结果也表明了所有的操作都是由同一个线程执行的,因此实现了并发执行。

将上面的sleep操作换成真正的IO操作,则实现了真正的异步IO,如下面的代码:

import asyncio

@asyncio.coroutine
def wget(host):
    print('wget %s...' % host)
    connect = asyncio.open_connection(host, 80)
    reader, writer = yield from connect
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
    writer.write(header.encode('utf-8'))
    yield from writer.drain()
    while True:
        line = yield from reader.readline()
        if line == b'\r\n':
            break
        print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
    # Ignore the body, close the socket
    writer.close()

loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

执行结果:

wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
...

三个连接由一个线程通过coroutine进行执行。

总结:asyncio提供了对异步IO的完整支持;异步操作需要通过在coroutine中通过yield from进行实现;多个coroutine可以组成列表放入EventLoop中并发执行。

特别注意基于生成器的协程支持将在python 3.10中移除,官方推荐使用下节将要介绍的async/await实现协程。
在这里插入图片描述
推荐学习:https://blog.csdn.net/SL_World/article/details/86597738

3 async/await

python3.5开始引入新的语法支持异步IO,分别是asyncawait,可以让coroutine的代码更加简洁易读。

具体替换项为:

  • @asyncio.coroutine替换为async
  • yield from替换为await

代码为:

import asyncio
import threading

async def hello_async():
    print('Hello World! (%s)' %threading.currentThread())
    r = await asyncio.sleep(1)
    print('Hello Again! (%s)' %threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello_async(),hello_async()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出为:

Hello World! (<_MainThread(MainThread, started 140213464598272)>)
Hello World! (<_MainThread(MainThread, started 140213464598272)>)
暂停约1秒
Hello Again! (<_MainThread(MainThread, started 140213464598272)>)
Hello Again! (<_MainThread(MainThread, started 140213464598272)>)

4 greenlet

官方文档

greenlets是轻量化的协程,可以单独使用,但通常是和gevent联合使用,提供高层抽象的api以及支持异步IO操作。

4.1 greenlet切换

greenlet类似于线程或Python的内置协程(生成器和async def函数)。通过显式的切换(类似于generator.send(val)),让不同的greenlet去执行与其自身对应的函数,switch时可以传递参数,如下面的代码所示:

>>> from greenlet import greenlet

>>> def test1():
...     print("[gr1] main  -> test1")
...     gr2.switch()
...     print("[gr1] test1 <- test2")
...     return 'test1 done'

>>> def test2():
...     print("[gr2] test1 -> test2")
...     gr1.switch()
...     print("This is never printed.")

>>> gr1 = greenlet(test1)
>>> gr2 = greenlet(test2)
>>> gr1.switch()
[gr1] main  -> test1
[gr2] test1 -> test2
[gr1] test1 <- test2
'test1 done'
>>> gr1.dead
True
>>> gr2.dead
False

4.2 greenlet生命周期

一个greenlet所指定的函数执行完成后,它的生命周期就结束了,如上面代码所示。可以同时创建多个greenlet,但同一时刻只有一个greenlet可以处于运行状态,其余所有的greenlet都处于等待状态,也就是协程中实际运行的唯一线程一个时刻只能在运行一个函数。可以通过getcurrent()函数获取当前正在运行的greenlet对象。

greenlet可以和python thread联合使用,一个thead拥有一个main greenlet和多个sub greenlets,隶属于同一个线程的greenlets之间可以相互switch,但不能跨线程进行greenletswitch。如下面的代码所示:

from greenlet import getcurrent
from threading import Thread,currentThread
from threading import Event
started = Event()
switched = Event()

class T(Thread):
    def run(self):
        print('sub thread : %s' % currentThread())
        print('sub thread\'s greenlet %s' %getcurrent())
        self.glet = getcurrent()
        started.set()
        switched.wait()

print('main thread : %s' % currentThread())
print('main thread\'s greenlet %s' %getcurrent())
t = T()
t.start()
_ = started.wait()
t.glet.switch()
switched.set()
t.join()

输出:

main thread : <_MainThread(MainThread, started 140349767833344)>
main thread's greenlet <greenlet.greenlet object at 0x7fa5b736b930>
sub thread : <T(Thread-7, started 140349624043264)>
sub thread's greenlet <greenlet.greenlet object at 0x7fa5b736ba60>

报错:

发生异常: error
cannot switch to a different thread
  File "/***.py", line 39, in <module>
    t.glet.switch()

但要注意,线程的main greenlet永远不会结束生命周期,因此,如果将一个线程的main greenlet的引用传递给了另一个线程,要仔细审核其生命周期,避免使用线程已结束的main greenlet

4.3 greenlet’s parents

main greenlet的parent是None。其余的greenlet都是有其parent的,谁创建了当前的greenlet,谁就是当前greenlet的parent。当前greenlet生命周期结束后,就回到其parent处。4.1节的示例代码中,gr1gr2的parent都是main greenlet,某一个结束后都是回到main greenlet

关于greenlet的理解还相当粗浅,后续使用到之后补充。

参考资料:
从yield from到async的使用
从0到1,Python异步编程的演进之路
Python Async/Await入门指南

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值