@Author: Runsen

一说并发,你肯定想到了多线程+进程模型,确实,多线程+进程,正是解决并发问题的经典模型之一。但对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。协程也是实现并发编程的一种方式。

协程

协程:是单线程下的并发,又称微线程。英文名是Coroutine。它和线程一样可以调度,但是不同的是线程的启动和调度需要通过操作系统来处理。

协程是一种比线程更加轻量级的存在,最重要的是,协程不被操作系统内核管理,协程是完全由程序控制的。

运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。

协程不需要多线程的锁机制,因为只有一个线程,不存在变量冲突。

对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。

注意协程这个概念完全是程序员自己想出来的东西,它对于操作系统来说根本不存在。操作系统只有进程和线程。

Python中使用协程的例子

yield关键字相当于是暂停功能,程序运行到yield停止,send函数可以传参给生成器函数,参数赋值给yield

def customer():
    while True:
        number = yield
        print('开始消费:',number)
        
custom = customer()
next(custom)
for i in range(5):
    print('开始生产:',i)
    custom.send(i)

结果如下

开始生产: 0
开始消费: 0
开始生产: 1
开始消费: 1
开始生产: 2
开始消费: 2
开始生产: 3
开始消费: 3
开始生产: 4
开始消费: 4

代码解析:

  1. 协程使用生成器函数定义:定义体中有 yield 关键字。
  2. yield 在表达式中使用;如果协程只需从客户custom接收数据,如果没有产出的值,那么产出的值是 None。
  3. 首先要调用 next(…) 函数,因为生成器还没启动,没在 yield 语句处暂停,所以一开始无法发送数据。
  4. 调用send方法,把值传给 yield 的变量,然后协程恢复,继续执行下面的代码,直到运行到下一个 yield 表达式,或者终止。

async和await

async和await是原生协程,是Python3.5以后引入的两个关键词。

async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

下面,我们从一个demo示例看起,具体代码如下。

import time 

def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    time.sleep(1)
    print("Maoli prints" + str(num) + " OK")

def main(nums):
    for num in nums:
        print_num(num)
%time main([i for i in range(1,6)])


Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5 s

%time 需要在jupyter notebook中运行,这是jupyter的语法糖。

上面代码是从上到下执行的。下面我们将上面的代码改成单线程协程版本。

注意py版本3.7以上,主要使用的是asyncio模块,如果出现AttributeError: module ‘asyncio‘ has no attribute ‘run‘报错,这是asyncio版本不兼容的原因,需要将Python版本提升至3.7以上。

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    for num in nums:
        await print_num(num)
%time asyncio.run(main([i for i in range(1,6)]))


Maoli is printing 1 nows
Maoli prints1 OK
Maoli is printing 2 nows
Maoli prints2 OK
Maoli is printing 3 nows
Maoli prints3 OK
Maoli is printing 4 nows
Maoli prints4 OK
Maoli is printing 5 nows
Maoli prints5 OK
Wall time: 5.01 s

asyncio.run() 函数用来运行最高层级的入口点 “main()” 函数。await 是同步调用等待一个协程。以下代码段会在等待 1 秒后打印 num,但在运行速度上没有发生改变。这里需要引入asyncio.create_task可等待对象才可以。

create_task

如果一个对象可以在 await 语句中使用,那么它就是可等待对象。

协程中的还一个重要概念,任务(Task)。

如果写一个数字是一个任务,那么毛利我要完成5个任务。

毛利我写个1-5都这么慢,不行,我要加速写。

asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    tasks = [asyncio.create_task(print_num(num)) for num in nums]
    for task in tasks:
        await task
%time asyncio.run(main([i for i in range(1,6)]))


Maoli is printing 1 nows
Maoli is printing 2 nows
Maoli is printing 3 nows
Maoli is printing 4 nows
Maoli is printing 5 nows
Maoli prints1 OK
Maoli prints3 OK
Maoli prints5 OK
Maoli prints2 OK
Maoli prints4 OK
Wall time: 1.01 s

还可以写成await asyncio.gather(*tasks)这种方法

import asyncio

async def print_num(num):
    print("Maoli is printing " + str(num) + " nows" )
    await asyncio.sleep(1)
    print("Maoli prints" + str(num) + " OK")

async def main(nums):
    tasks = [asyncio.create_task(print_num(num)) for num in nums]
    await asyncio.gather(*tasks)
%time asyncio.run(main([i for i in range(1,6)]))

*tasks 解包列表,将列表变成了函数的参数;与之对应的是, ** dict 将字典变成了函数的参数。

协程的写法简洁清晰,只要把 async / await 语法和 create_task 结合来用,就是Python中比较常见的协程写法。


https://docs.python.org/zh-cn/3/library/asyncio.html