python中的asyncio使用详解与异步协程的处理流程分析

在python的异步实践中,每次看asynicio都会有新的收获,本篇总结一下最近看这个库的使用。

一些核心概念

异步函数的定义

普通函数的定义是使用 def 关键词,异步的函数,协程函数(Coroutine)本质上是一个函数,特点是在代码块中可以将执行权交给其他协程,使用async def 来定义

# 普通函数定义
def add2(x):
    print(x+2)
    return x+2

# 异步函数的定义
async def add3(x):
    print("in async fun add")
    return x+3

如何调用协程并且得到它的运行结果? 调用普通的函数只需要 result = add2(2),这时函数就可以得到运行,并且将结果4返回给result,如果使用result = add3(2),此时再打印 result 呢? 得到的是一个coroutine对象,<coroutine object add3 at 0x000002ed564a5048>,并不是2+3=5这个结果,怎样才能得到结果呢? 协程函数想要执行需要放到事件循环里执行。

事件循环 Eventloop

Eventloop 是asyncio应用的核心,把一些异步函数注册到这个事件循环上,事件循环会循环执行这些函数,当执行到某个函数时,如果它正在等待I/O返回,如它正在进行网络请求,或者sleep操作,事件循环会暂停它的执行去执行其他的函数;当某个函数完成I/O后会恢复,下次循环到它的时候继续执行。因此,这些异步函数可以协同(Cooperative)运行:这就是事件循环的目标。

返回到上面的函数,想要得到函数执行结果,需要有一个Eventloop

import asyncio

loop = asyncio.get_event_loop()

async def add3(x):
    print("in async fun add")
    return x+3

result = loop.run_until_complete(add3(2))
print(result)

#运行的结果是
#in async fun add
#5

或者使用await 关键字来修饰函数的调用,如result = await add3(2),但是await只能用在协程函数中,所以想要用await关键字就还需要定义一个协程函数

async def add3(x):
    print("in async fun add")
    return x+3

async def main():
    result = await add3(2)
    return result

但最终的执行还是需要放到一个事件循环中进行.

稍微复杂一点的例子

# coding:utf-8

import asyncio
import time


async def testa(x):
    print("in test a")
    await asyncio.sleep(3)
    print("Resuming a")
    return x


async def testb(x):
    print("in test b")
    await asyncio.sleep(1)
    print('Resuming b')
    return x


async def main():
    start = time.time()
    resulta = await testa(1)
    resultb = await testb(2)
    print("test a result is %d"%resulta)
    print("test b result is %d"%resultb)
    print("use %s time"%(time.time()-start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

这段代码定义了两个协程,并将它们放到另外一个协程main函数中,想要获得它们运行的结果,事件循环的特点是当它遇到某个I/O需要等待(如这里的asyncio.sleep()函数)的时候,可以去执行其它的函数,这样,整个函数执行所需要的时间,应该是所有协程中执行时间最长的那个,对于上面这个代码来说,一个sleep了3秒,一个sleep了1秒,总的用时应该是3秒多一点,但结果是这样吗?

它的输出是这样的

in test a
Resuming a
in test b
Resuming b
test a result is 1
test b result is 2
use 4.001966714859009 time

它的用时是4秒多一点,而且是先执行了testa函数,然后再执行了testb函数,是串行的依次执行的,并没有像我们想象中的并发执行。那应该怎样才能并发执行呢?

async def main():
    start = time.time()
    resulta,resultb = await asyncio.gather(testa(1),testb(2))
    print("test a result is %d" % resulta)
    print("test b result is %d" % resultb)
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

需要将协程放到asyncio.gather() 中运行,上面的代码得到的输出是

in test b
in test a
Resuming b
Resuming a
test a result is 1
test b result is 2
use 3.001237392425537 time

可以看到,testa和testb是同步在运行,由于testb只sleep了1秒钟,所以testb先输出了Resuming b,最后将每个协程函数的结果返回,注意,这里是gather()函数里的每一个协程函数都执行完了,它才结果,结果是一个列表,列表里的值顺序和放到gather函数里的协程的顺序是一致的。

除了使用asyncio.gather 来执行协程函数以外,还可以使用Task任务对象

async def main():
    start = time.time()

    taska = asyncio.ensure_future(testa(1))
    taskb = asyncio.ensure_future(testb(2))

    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    await taskb
    await taska
    print(taska.done(), taskb.done())

    print(taskb.result())
    print(taska.result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

使用asyncio.ensure_future(testa(1)) 返回一个task对象,此时task进入pending状态,并没有执行,这时print(taska) 得到<task pending coro="<testa()" running at f: python python3test asynctest.py:7>&gt; 些时,taska.done()返回False,表示它还没有结束,当调用await taska 时表示开始执行该协程,当执行结束以后,taska.done() 返回True,这时可以调用taska.result() 得到函数的返回值,如果协程还没有结束就调用result()方法则会抛个异常,raise InvalidStateError('Result is not ready.').

创建task对象除了使用asyncio.ensure_future()方法还可以使用loop.create_task() 方法

async def main():
    start = time.time()

    taska = loop.create_task(testa(1))
    taskb = loop.create_task(testb(2))

    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    await taskb
    await taska
    print(taska.done(), taskb.done())

    print(taskb.result())
    print(taska.result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

上面一直在使用asyncio.gather()函数来执行协程函数,还有一个asyncio.wait()函数,它的参数是协程的列表。

async def main():
    start = time.time()
    done,pending = await asyncio.wait([testa(1),testb(2)])
    print(list(done))
    print(list(pending))
    print(list(done)[0].result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

asyncio.wait() 返回一个tuple对象,对象里又包含一个已经完成的任务set和未完成任务的set,上面代码得到的结果是

in test b
in test a
Resuming b
Resuming a
[<task finished coro="<testa()" done, defined at f: python python3test asynctest.py:7> result=1>, <task finished coro="<testb()" done, defined at f: python python3test asynctest.py:14> result=2>]
[]
1
use 3.0003058910369873 time

使用wait和gather有哪些区别呢? 首先,gather是需要所有任务都执行结束,如果某一个协程函数崩溃了,则会抛异常,都不会有结果。 wait可以定义函数返回的时机,可以是FIRST_COMPLETED(第一个结束的), FIRST_EXCEPTION(第一个出现异常的), ALL_COMPLETED(全部执行完,默认的)

# coding:utf-8

import asyncio
import time


async def testa(x):
    print("in test a")

    await asyncio.sleep(3)
    print(1/0)
    print("Resuming a")
    return x


async def testb(x):
    print("in test b")
    await asyncio.sleep(1)
    print(1/0)
    print('Resuming b')
    return x


async def main():
    start = time.time()
    done,pending = await asyncio.wait([testa(1),testb(2)],return_when=asyncio.tasks.FIRST_EXCEPTION)
    print(list(done))
    print(list(pending))
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

这段代码要求在出现第一个异常的时候就结果,函数整体不会崩溃,只是如果这里想要获取结果的话它是一个异常对象。

in test b
in test a
[<task finished coro="<testb()" done, defined at f: python python3test asynctest.py:16> exception=ZeroDivisionError('division by zero',)>]
[<task pending coro="<testa()" running at f: python python3test asynctest.py:10> wait_for=<future pending cb="[Task._wakeup()]">>]
use 1.0000195503234863 time

可以在实际的工作中,由于以前写了太多的多线程与多进程,所以对于以前编写风格和一些由于没有异步支持的库函数来说,由于要写在异步里,所以对于编写代码来说还是要处理很多同步的方法,今天在这里整理一下在异步操作中如果处理同步的函数问题。

为了更好的演示,我准备了三个函数,一个同步的函数,两个异步的函数

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行")
    time.sleep(2)
    os.system("ping %s"%url)
    print("阻塞函数运行结束")
    
# 定义两个异步函数
async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
async def asyncfunc2():
    print("Suspending func2")
    await asyncio.sleep(1)
    print("func func2 ", threading.current_thread())
    print('Resuming func2')
    return "func2"

协程中控制任务

单个协程任务的运行

上面的函数,比如说我只想将asyncfunc1() 函数运行并且得结果,可以使用loop.create_task()方法创建一个task对象,task是Futures的子类,当调用loop.run_until_complete() 以后,协程跑完以后,通过task.result()获取协程函数的返回结果。

async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"

if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    task = loop.create_task(asyncfunc1())
    loop.run_until_complete(task)
    print("task result is ",task.result())

输出结果为

In main thread  <_MainThread(MainThread, started 6140)>
Suspending func1
func func1  <_MainThread(MainThread, started 6140)>
Resuming func1
task result is  func1

主线程和跑的协程函数是在同一个线程中。

也可以给task对象添加一个回调方法

#coding:gbk
import asyncio
import time,sys


async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
# 定义一个回调函数
def callbackfunc(task):
    print("task 运行结束,它的结果是:",task.result())
    
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    task = loop.create_task(asyncfunc1())
    task.add_done_callback(callbackfunc)
    loop.run_until_complete(task)

输出结果为

In main thread  <_MainThread(MainThread, started 11248)>
Suspending func1
func func1  <_MainThread(MainThread, started 11248)>
Resuming func1
task 运行结束,它的结果是: func1

loop.run_until_complete 是一个阻塞方法,只有当它里面的协程运行结束以后这个方法才结束,才会运行之后的代码。

其实也可以不调用loop.run_until_complete方法,创建一个task以后,其实就已经在跑协程函数了,只不过当事件循环如果准备开始运行了,此时的task状态是pending,如果不调用事件循环的话,则不会运行协程函数,由于主线程跑完了,子线程也就被销毁了,如代码写成这样:

if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    task = loop.create_task(asyncfunc1())
    time.sleep(3)

得到的输出是

In main thread  <_MainThread(MainThread, started 6056)>
Task was destroyed but it is pending!
task: <task pending coro="<asyncfunc1()" running at test.py:18> cb=[callbackfunc() at test.py:39]>
sys:1: RuntimeWarning: coroutine 'asyncfunc1' was never awaited

所以想要使得协程函数得到执行,需要调用事件循环来执行任务,上面的loop.run_until_complete就是使循环开始跑了,其实也可以使用loop.run_forever(),这个函数就像它的名字一样,会一直跑。只有事件循环跑起来了,那么使用该循环注册的协程才会得到执行,但是如果使用loop.run_forever()则会阻塞在这里,事件循环还有一个stop方法,可以结束循环,我们可以在task对象上添加一个回调方法,当协程执行结束以后,调用事件循环的stop方法来结束整个循环

#coding:gbk
import asyncio
import time,sys

async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
# 定义一个回调函数
def callbackfunc(task):
    print("task 运行结束,它的结果是:",task.result())
    loop.stop()
    
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    task = loop.create_task(asyncfunc1())
    task.add_done_callback(callbackfunc)
    loop.run_forever()

除了使用loop.run_until_complete方法,还可以使用asyncio.ensure_future() 方法来运行协程,将上面代码中的task = loop.create_task(asyncfunc1()) 改为 task = asyncio.ensure_future(asyncfunc1())会得到相同的结果,它的参数是协程对象或者futures,也可以传task对象,因为task是futures的子类,当传入的是一个协程对象时,返回一个task对象,传入一个futures的时候,直接返回futures对象,也就是说,在调用asyncio.ensure_future()以后,都会返回一个task对象,都可以为它添加一个回调方法,并且可以调用task.result()方法得到结果(注意如果task没有执行结束就调用result方法,则会抛异常)。

多个协程任务的并行

最上面我准备了两个异步的函数asyncfunc1和asyncfunc2,如果我想要这两个函数同时执行,并且得到它们的返回值该怎么操作呢? 有了上面单协程的经验,我们也可以使用事件循环创建两个task,然后在run_forever()来执行,可以对task添加回调,将结果输出。

#coding:gbk
import asyncio

# 定义两个异步函数
async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
async def asyncfunc2():
    print("Suspending func2")
    await asyncio.sleep(1)
    print("func func2 ", threading.current_thread())
    print('Resuming func2')
    return "func2"

    
# 定义一个回调函数
def callbackfunc(task):
    print("task 运行结束,它的结果是:",task.result())
    
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    task1 = loop.create_task(asyncfunc1())
    task1.add_done_callback(callbackfunc)
    task2 = loop.create_task(asyncfunc2())
    task2.add_done_callback(callbackfunc)
    loop.run_forever()

输出结果是

In main thread  <_MainThread(MainThread, started 8040)>
Suspending func1
Suspending func2
func func1  <_MainThread(MainThread, started 8040)>
Resuming func1
func func2  <_MainThread(MainThread, started 8040)>
Resuming func2
task 运行结束,它的结果是: func1
task 运行结束,它的结果是: func2

此时由于loop调用了run_forever方法,且没有方法调用stop方法,所以程序会一直卡着。

这样是可以将多个协程跑起来,但这样的处理一是繁琐,二是不方便结果的回收。

asyncio有一个gather方法,可以传入多个任务对象,当调用await asyncio.gather(*) 时,它会将结果全部返回

由于await 只能写在async def 函数中,所以这里还需要新创建一个函数

async def main():
    task1 = loop.create_task(asyncfunc1())
    task1.add_done_callback(callbackfunc)
    task2 = loop.create_task(asyncfunc2())
    task2.add_done_callback(callbackfunc)
    result = await asyncio.gather(task1,task2)    
    print(result)
    
async def mian2():
    result = await asyncio.gather(asyncfunc1(),asyncfunc2())
    print(result)

两种定义方式都可以,一个是向gather函数传的是协程对象,一个是传的task对象。之后在调用

if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main()) # or main2()

得到的输出为

In main thread  <_MainThread(MainThread, started 7016)>
Suspending func1
Suspending func2
func func1  <_MainThread(MainThread, started 7016)>
Resuming func1
func func2  <_MainThread(MainThread, started 7016)>
Resuming func2
task 运行结束,它的结果是: func1
task 运行结束,它的结果是: func2
['func1', 'func2']

接下来介绍在异步的应用里如何调用同步的函数。 依然是之前准备的三个函数,一个阻塞的,两个异步的。

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行")
    time.sleep(2)
    os.system("ping %s"%url)
    print("阻塞函数运行结束")
    
# 定义两个异步函数
async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
async def asyncfunc2():
    print("Suspending func2")
    await asyncio.sleep(1)
    print("func func2 ", threading.current_thread())
    print('Resuming func2')
    return "func2"

使用传统的多线程的方式跑同步代码

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行,当前的线程ID为:",threading.current_thread())
    time.sleep(2)
    os.system("ping %s"%url)
    print("阻塞函数运行结束")
    
    
async def main():
    task1 = loop.create_task(asyncfunc1())
    task1.add_done_callback(callbackfunc)
    task2 = loop.create_task(asyncfunc2())
    task2.add_done_callback(callbackfunc)
    result = await asyncio.gather(task1,task2)    
    print(result)
    
async def mian2():
    result = await asyncio.gather(asyncfunc1(),asyncfunc2())
    print(result)
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    # 创建三个子线程
    t1 = threading.Thread(target=ping,args=("www.baidu.com",))
    t2 = threading.Thread(target=ping,args=("www.yangyanxing.com",))
    t3 = threading.Thread(target=ping,args=("www.qq.com",))
    t1.start()
    t2.start()
    t3.start()

输出结果

In main thread  <_MainThread(MainThread, started 9208)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-1, started 8720)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-2, started 9368)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-3, started 8320)>

正在 Ping https.qq.com [123.151.137.18] 具有 32 字节的数据:
来自 123.151.137.18 的回复: 字节=32 时间=4ms TTL=53

正在 Ping www.a.shifen.com [220.181.38.150] 具有 32 字节的数据:
来自 220.181.38.150 的回复: 字节=32 时间=1ms TTL=54

正在 Ping yangyanxing.coding.me [119.28.76.36] 具有 32 字节的数据:
....
....
阻塞函数运行结束

可以看到,主线程和子线程跑在了不同的线程中。

在事件循环中动态的添加同步函数

解决方案是,先启一个子线程,这个线程用来跑事件循环loop,然后动态的将同步函数添加到事件循环中

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义阻塞的函数
print("阻塞函数开始运行,当前的线程ID为:",threading.current_thread())
    time.sleep(2)
    print("模拟ping 输出 ",url)
    print("阻塞函数运行结束,当前的线程ID为:",threading.current_thread())  

#定义一个跑事件循环的线程函数    
def start_thread_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环,让它run_forever
    t = threading.Thread(target= start_thread_loop, args=(loop,))
    t.start()
    
    # 在主线程中动态添加同步函数
    loop.call_soon_threadsafe(ping,"www.baidu.com")
    loop.call_soon_threadsafe(ping,"www.qq.com")
    loop.call_soon_threadsafe(ping,"www.yangyanxing.com")
    print('主线程不会阻塞')

由于使用ping 命令得到很多输出,所以我对函数稍稍做了修改,只是模拟打印了一行文字,但是函数中的time.sleep(2) 这个是一个阻塞式的函数 得到的输出为

In main thread  <_MainThread(MainThread, started 7924)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-1, started 10716)>
主线程不会阻塞
模拟ping 输出  www.baidu.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-1, started 10716)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-1, started 10716)>
模拟ping 输出  www.qq.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-1, started 10716)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-1, started 10716)>
模拟ping 输出  www.yangyanxing.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-1, started 10716)>

从输出结果可以看出,loop.call_soon_threadsafe()和主线程是跑在同一个线程中的,虽然loop.call_soon_threadsafe()没有阻塞主线程的运行,但是由于需要跑的函数ping是阻塞式函数,所以调用了三次,这三次结果是顺序执行的,并没有实现并发。 如果想要实现并发,需要通过run_in_executor 把同步函数在一个执行器里去执行。该方法需要传入三个参数,run_in_executor(self, executor, func, *args) 第一个是执行器,默认可以传入None,如果传入的是None,将使用默认的执行器,一般执行器可以使用线程或者进程执行器。

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行,当前的线程ID为:",threading.current_thread())
    time.sleep(2)
    print("模拟ping 输出 ",url)
    print("阻塞函数运行结束,当前的线程ID为:",threading.current_thread())
    

#定义一个跑事件循环的线程函数    
def start_thread_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环,让它run_forever
    t = threading.Thread(target= start_thread_loop, args=(loop,))
    t.start()
    
    # 在主线程中动态添加同步函数
    loop.run_in_executor(None,ping,"www.baidu.com")
    loop.run_in_executor(None,ping,"www.qq.com")
    loop.run_in_executor(None,ping,"www.yangyanxing.com")
    print('主线程不会阻塞')

得到的输出结果

In main thread  <_MainThread(MainThread, started 8588)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-2, started daemon 9068)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-3, started daemon 7200)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-4, started daemon 10924)>
主线程不会阻塞
模拟ping 输出  www.yangyanxing.com
模拟ping 输出  www.baidu.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-4, started daemon 10924)>
阻塞函数运行结束,当前的线程ID为: <thread(thread-2, started daemon 9068)>
模拟ping 输出  www.qq.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-3, started daemon 7200)>

可以看到同步函数实现了并发,但是它们跑在了不同的线程中,这个就和之前传统的使用多线程是一样的了。

上文说到,run_in_executor的第一个参数是执行器,这里执行器是使用concurrent.futures 下的两个类,一个是thread一个是process,也就是执行器可以分为线程执行器和进程执行器。它们在初始化的时候都有一个max_workers参数,如果不传则根据系统自身决定。

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行,当前的线程ID为:",threading.current_thread(),"进程ID为:",os.getpid())
    time.sleep(2)
    print("模拟ping 输出 ",url)
    print("阻塞函数运行结束,当前的线程ID为:",threading.current_thread())
    

#定义一个跑事件循环的线程函数    
def start_thread_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环,让它run_forever
    t = threading.Thread(target= start_thread_loop, args=(loop,))
    t.start()
    threadingexecutor = concurrent.futures.ThreadPoolExecutor(2)
    processExetutor = concurrent.futures.ProcessPoolExecutor()
    
    # 在主线程中动态添加同步函数
    loop.run_in_executor(processExetutor,ping,"www.baidu.com")
    loop.run_in_executor(processExetutor,ping,"www.qq.com")
    loop.run_in_executor(processExetutor,ping,"www.yangyanxing.com")
    print('主线程不会阻塞')

这里初始化了两个执行器,一个是线程的,一个是进程的, 它们执行的效果一样,只是一个跑在了多线程,一个跑在了多进程 使用concurrent.futures.ThreadPoolExecutor()执行器的结果是

In main thread  <_MainThread(MainThread, started 7688)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-2, started daemon 10924)> 进程ID为: 8188
阻塞函数开始运行,当前的线程ID为: <thread(thread-3, started daemon 9068)> 进程ID为: 8188
主线程不会阻塞
模拟ping 输出  www.baidu.com
模拟ping 输出  www.qq.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-2, started daemon 10924)>
阻塞函数运行结束,当前的线程ID为: <thread(thread-3, started daemon 9068)>
阻塞函数开始运行,当前的线程ID为: <thread(thread-2, started daemon 10924)> 进程ID为: 8188
模拟ping 输出  www.yangyanxing.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-2, started daemon 10924)>

这们的进程ID都是8188,是跑在了同一个进程下。另外注意一下,我这里在初始化的时候传一个max_workers为2,注意看结果的输出,它是先执行了前两个,当有一个执行完了以后再开始执行第三个,而不是三个同时运行的。

使用concurrent.futures.ProcessPoolExecutor()执行器的执行结果

In main thread  <_MainThread(MainThread, started 10220)>
主线程不会阻塞
阻塞函数开始运行,当前的线程ID为: <_MainThread(MainThread, started 3928)> 进程ID为: 6652
阻塞函数开始运行,当前的线程ID为: <_MainThread(MainThread, started 10992)> 进程ID为: 9436
阻塞函数开始运行,当前的线程ID为: <_MainThread(MainThread, started 9740)> 进程ID为: 9000
模拟ping 输出  www.qq.com
阻塞函数运行结束,当前的线程ID为: <_MainThread(MainThread, started 3928)>
模拟ping 输出  www.baidu.com
阻塞函数运行结束,当前的线程ID为: <_MainThread(MainThread, started 10992)>
模拟ping 输出  www.yangyanxing.com
阻塞函数运行结束,当前的线程ID为: <_MainThread(MainThread, started 9740)>

可以看出来它们的进程ID是不同的。

这样看使用run_in_executor和使用多进程和多线程其实意义是一样的。别着急,在讲完异步函数以后就可以看到区别了。

在事件循环中动态的添加异步函数

通过asyncio.run_coroutine_threadsafe 方法来动态的将一个协程绑定到事件循环上,并且不会阻塞主线程

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义两个异步函数
async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
async def asyncfunc2():
    print("Suspending func2")
    await asyncio.sleep(1)
    print("func func2 ", threading.current_thread())
    print('Resuming func2')
    return "func2"
    

#定义一个跑事件循环的线程函数    
def start_thread_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环,让它run_forever
    t = threading.Thread(target= start_thread_loop, args=(loop,))
    t.start()
    asyncio.run_coroutine_threadsafe(asyncfunc1(),loop)
    asyncio.run_coroutine_threadsafe(asyncfunc1(),loop)
    asyncio.run_coroutine_threadsafe(asyncfunc2(),loop)
    asyncio.run_coroutine_threadsafe(asyncfunc2(),loop)
    
    print('主线程不会阻塞')

通过asyncio.run_coroutine_threadsafe在loop上绑定了四个协程函数,得到的输出结果为

In main thread  <_MainThread(MainThread, started 4772)>
Suspending func1
主线程不会阻塞
Suspending func1
Suspending func2
Suspending func2
func func1  <thread(thread-1, started 3948)>
Resuming func1
func func2  <thread(thread-1, started 3948)>
Resuming func2
func func1  <thread(thread-1, started 3948)>
Resuming func1
func func2  <thread(thread-1, started 3948)>
Resuming func2

主线程不会被阻塞,起的四个协程函数几乎同时返回的结果,但是注意,协程所在的线程和主线程不是同一个线程,因为此时事件循环loop是放到了另外的子线程中跑的,所以此时这四个协程放到事件循环的线程中运行的。 注意这里只有run_coroutine_threadsafe方法,没有run_coroutine_thread 方法。

获取协程的返回结果

获取结果可以使用asyncio.gather()方法,这里面传的是coros_or_futures就是协程或者task对象,asyncio.run_coroutine_threadsafe()run_in_executor()返回的都是Future对象,所以可以将它们共同放到gather里,获取返回值.

#coding:gbk
import asyncio
import time,sys
import threading
import concurrent
import functools
import subprocess
import os

# 定义阻塞的函数
def ping(url):
    print("阻塞函数开始运行,当前的线程ID为:",threading.current_thread(),"进程ID为:",os.getpid())
    time.sleep(4)
    print("模拟ping 输出 ",url)
    print("阻塞函数运行结束,当前的线程ID为:",threading.current_thread())
    return url
    
# 定义两个异步函数
async def asyncfunc1():
    print("Suspending func1")
    await asyncio.sleep(1)
    print("func func1 ", threading.current_thread())
    print('Resuming func1')
    return "func1"
    
async def asyncfunc2():
    print("Suspending func2")
    await asyncio.sleep(2)
    print("func func2 ", threading.current_thread())
    print('Resuming func2')
    return "func2"
    

#定义一个跑事件循环的线程函数    
def start_thread_loop(loop):
    print("loop线程 id 为",threading.current_thread())
    asyncio.set_event_loop(loop)
    loop.run_forever()
    
# 定义一个回调函数
def callbackfunc(task):
    print("task 运行结束,它的结果是:",task.result())
    # loop.stop()
    
async def main():
    t1 = time.time()
    # 使用loop.create_task创建task对象,返回asyncio.tasks.Task对象
    task1 = loop.create_task(asyncfunc1())
    task2 = loop.create_task(asyncfunc2())
    # 使用asyncio.run_coroutine_threadsafe 返回的是concurrent.futures._base.Future对象
    # 注意这个对象没有__await__方法,所以不能对其使用await 但是可以给它添加回调add_done_callback
    task3 = asyncio.run_coroutine_threadsafe(asyncfunc1(),loop)
    task4 = asyncio.run_coroutine_threadsafe(asyncfunc2(),loop)
    
    # 使用loop.run_in_executor创建阻塞的任务,返回asyncio.futures.Future对象
    task5 = loop.run_in_executor(None,ping,"www.baidu.com")
    task6 = loop.run_in_executor(None,ping,"www.yangyanxing.com")
    
    # 使用asyncio.ensure_future()创建任务对象
    task7 = asyncio.ensure_future(asyncfunc1())
    task8 = asyncio.ensure_future(asyncfunc2())
    
    
    task1.add_done_callback(callbackfunc)    
    task2.add_done_callback(callbackfunc)    
    task3.add_done_callback(callbackfunc)
    task4.add_done_callback(callbackfunc)
    task5.add_done_callback(callbackfunc)
    task6.add_done_callback(callbackfunc)
    task7.add_done_callback(callbackfunc)
    task8.add_done_callback(callbackfunc)
   
    result = await asyncio.gather(task1,task2,task5,task6,task7,task8)    
    print(result)
    t2 = time.time()
    print("一共用了%s时间"%(t2-t1))
    
async def mian2():
    result = await asyncio.gather(asyncfunc1(),asyncfunc2(),)
    print(result)
    
def shutdown(loop):
    loop.stop()
    
if __name__=="__main__":
    print("In main thread ",threading.current_thread())
    loop = asyncio.get_event_loop()
    loop2 = asyncio.new_event_loop()
    # 在子线程中运行事件循环,让它run_forever
    t = threading.Thread(target= start_thread_loop, args=(loop,))
    t.start()
    asyncio.run_coroutine_threadsafe(main(),loop)
    print('主线程不会阻塞')

代码执行结果:

In main thread  <_MainThread(MainThread, started 6052)>
loop线程 id 为 <thread(thread-1, started 2388)>
主线程不会阻塞
阻塞函数开始运行,当前的线程ID为: <thread(thread-2, started daemon 11644)> 进程ID为: 12280
阻塞函数开始运行,当前的线程ID为: <thread(thread-3, started daemon 1180)> 进程ID为: 12280
Suspending func1
Suspending func2
Suspending func1
Suspending func2
Suspending func1
Suspending func2
func func1  <thread(thread-1, started 2388)>
Resuming func1
func func1  <thread(thread-1, started 2388)>
Resuming func1
func func1  <thread(thread-1, started 2388)>
Resuming func1
task 运行结束,它的结果是: func1
task 运行结束,它的结果是: func1
task 运行结束,它的结果是: func1
func func2  <thread(thread-1, started 2388)>
Resuming func2
func func2  <thread(thread-1, started 2388)>
Resuming func2
func func2  <thread(thread-1, started 2388)>
Resuming func2
task 运行结束,它的结果是: func2
task 运行结束,它的结果是: func2
task 运行结束,它的结果是: func2
模拟ping 输出  www.baidu.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-2, started daemon 11644)>
模拟ping 输出  www.yangyanxing.com
阻塞函数运行结束,当前的线程ID为: <thread(thread-3, started daemon 1180)>
task 运行结束,它的结果是: www.baidu.com
task 运行结束,它的结果是: www.yangyanxing.com
['func1', 'func2', 'www.baidu.com', 'www.yangyanxing.com', 'func1', 'func2']
一共用了4.002800464630127时间

总的时间是取决于所有运行的函数中耗时最长的,这里同步函数有个阻塞的sleep(4) ,所以总的时间是4秒多一点点.

关于在异步协程中的处理流程先总结这么多,之后再学习总结一个与异步相关的各种库如aiohttp的使用等等.

参考文章 深入理解asyncio(一)