Python学习笔记35:使用Asyncio处理并发
从标题也可以看出,Asyncio包和Futures包一样,也是用于处理并发的,但是在实现机制和使用方式上都有很大差别,所以会在接下来对两者进行对比说明。
但在介绍Asyncio包之前,我们先要弄清楚一些必须的基础概念。
这里不打算按照《Fluent Python》同名章节的知识点安排,我觉得一上来就介绍Asyncio包很容易把人弄晕,因为我自己在阅读的时候都被弄的一头雾水。
基础概念
同步&异步
关于同步和异步的概念,我想在我的工作经历中遇到过最多的和最熟悉的无非是AJAX调用了:
<script src="https://libs.baidu.com/jquery/1.10.2/jquery.min.js"></script>
<script>
$.ajax({
url: "http://myweb.com/index.php",
data: {
name: "Han Meimei" },
dataType: "json",
async: true,
timeout: 30000,
success: function(data){
console.log(data)
},
error: function(jqxhr,status,error){
print(status);
print(error);
}
});
console.log("ajax call is over")
</script>
这里的async
参数就是指定Jquery调用的AJAX是同步还是异步方式。
想了解JQuery如何调用ajax可以阅读jQuery ajax - ajax() 方法。
我们可以用上边的javascript代码创建一个html文件来进行测试,服务器还是用我们在Python学习笔记34:使用Futures处理并发中创建的那个用于查询个人信息的本地服务器。
这里需要注意的是服务器端代码需要简单改动一下:
header("Access-Control-Allow-Origin:*"); header("Access-Control-Allow-Credentials:true");
这两行代码是打开跨域支持,因为这里我们创建的html文件并不是放在服务器目录下边的,是通过> 浏览器直接运行本地文件的方式,此时url为
D://xxx/xxx/xxx.html
,这个url的域名显然和> 服务器myweb.com
不在一个域名下,所以正常情况下为了避免跨域攻击,服务端是不会接受这> 种跨域请求的,直接调用会出现AJAX调用失败,使用上边代码在服务端打开跨域支持后就可以> 正常调用了。
调试的时候可以使用VSCode的chrome插件,VSCode的配置文件launch.json
可以使用如下信息进行配置:
{
"name": "chrome: 当前笔记",
"type": "chrome",
"request": "launch",
"file": "${workspaceFolder}\\note35\\sync_and_async.html",
"cwd": "${workspaceFolder}\\note35\\"
}
打开chrome的调试工具箱可以清楚看到:
控制台先输出了ajax call is over
,此时该html页面的主js进程已经执行完毕了,浏览器的标签页也不会有一直运行的标识,但是实际上ajax调用还在进行,等待一会后就能看到console输出的ajax调用结果。
如果是同步的方式呢?
这里只需要把html中的js代码async: true,
改为async: false,
即可。
运行后就能看到标签页一直是“转圈圈”的状态,运行好一会后才会一起输出ajax调用结果以及ajax call is over
。
这也是web开发新手很容易犯的错误,因为js只有一个主线程,并不支持多线程,所以新手程序员很容易用同步的方式调用网络资源,此时会阻塞主线程,导致整个html页面无响应。
所以这里很容易得出一个结论,在面临阻塞式I/O调用的时候,采用异步的方式是个好主意。
我们在Python学习笔记34:使用Futures处理并发中已经了解到,对待I/O密集型任务,多线程可以很好地处理,而这里说了,异步也可以,那异步和多线程比较有何优缺点呢?
多线程&异步
首先要明确的是,异步和多线程最大的不同是其作用机制。
多线程是通过开启多个线程,就像分身术一样,本来一个人干的活让多个人同时去做,必然可以大大提升效率。而异步就不一样了,自始至终都只是一个线程,也就是一个人在干活,但是这个人比较聪明,遇到阻塞型事务,比如在柜台前排队办理银行业务的时候,它不会傻等着,它会掏出笔记本写写日记,或者更有可能的是用手机打一把炉石…
可以看出这是两种截然不同的处理思路。
而同样的,因为这种处理问题的方式的不同,其特点也不一样。
对于多线程,可以充分挖掘多核心处理器的优势,只要软硬件允许,就能发挥最大性能,毕竟人家是在用确确实实的“分身术”。但这也会带来其它问题,比如资源消耗问题。因为你的分身不可能不吃饭白干活,就算忍者也要消耗查克拉不是。同样的,多线程会带来一些资源开销,比如要使用额外的内存去维持多出来的线程的数据和状态。这种开销不是说在单个线程中的数据之和的开销,而是会起到1+1>2的效果,因为操作系统管理线程本身也需要一些额外开销。
对于异步来说,它没法发挥出多核处理器的全部性能,因为只有一个线程,没法让其它核心一起来干活。但同样的,它也不需要那些额外线程产生的额外开销。
异步的底层实现依然是通过多线程,可能需要操作系统开启一个线程通过轮询的方式定期检查异步调用的执行结果,如果有结果了就调用相应的回调函数,比如示例中的
success
和fail
。但对于应用层来说这些都是无需关心的底层实现,其开销依然要低于多线程。
当然,上面的这些分析都是理想状态下的,具体到Python,我们在Python学习笔记34:使用Futures处理并发中已经提到了,因为Python是线程不安全的,所以存在GIL控制多线程,让多线程在事实上以单线程的方式执行,只有在I/O读写或sleep
等阻塞情况下才会暂时地释放GIL,执行其它线程。在这种情形下,显然使用异步是个更优秀的解决方案,在GIL下,拥有相似的执行效率,但是又无需额外资源开销。
而Python对于异步编程给出的解决方案就是Asyncio包。
信息查询的Asyncio实现
需要安装一个第三方包aiohttp
:
pip install aiohttp
这里直接给出使用asycio包异步调用的示例代码,稍后我们仔细分析如何实现:
import asyncio
import aiohttp
from aiohttp import web
import pprint
async def getPersonInfo(name):
url = "http://myweb.com/?name={}".format(name)
personInfo = {
}
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
queryResult = await response.json()
if queryResult['status'] == 'success':
personInfo = queryResult['result']
elif response.status == 404:
raise web.HTTPNotFound
else:
raise web.HTTPServerError
return personInfo
def getPeopleInfo(loop, names):
queryCoroutines = [getPersonInfo(name) for name in names]
queryDone = asyncio.wait(queryCoroutines)
futuresDone,_ = loop.run_until_complete(queryDone)
results = {
}
for future in</