我们在上一章学习了Python并发编程的一种实现方法——多线程。今天,我们趁热打铁,看看Python并发编程的另一种实现方式——Asyncio。和前面协程的那章不太一样,这节课我们更加注重原理的理解。

通过上节课的学习,我们知道在进行I/O操作的时候,使用多线程与普通的单线程比较,效率有了很大的提高,既然这样,为什么还要Asyncio呢?

虽然多线程有诸多优点并且应用广泛,但是也存在一定的局限性:

※多线程运行过程很容易被打断,因此有可能出现race condition的情况

※线程的切换存在一定的消耗,线程数量不能无限增加,因此,如果I/O操作非常密集,多线程很有可能满足不了高效率、高质量的需求。

针对这些问题,Asyncio应运而生。

什么是Asyncio?

Sync VS Async

我们首先来区分一下Sync(同步)和Async(异步)的概念。

※所谓Sync,是指操作一个接一个的执行,下一个操作必须等上一个操作完成后才能执行。

※而Async是指不同操作之间可以相互交替执行,如果某个操作被block,程序并不会等待,而是会找出可执行的操作继续执行。


举个简单的例子,我们要做一个报表并用邮件发送给老板,看看两种方式有什么不同:

※按照Sync的方式,我们相软件里输入各项数据,然后等5分钟生成了报表明细以后,再写邮件发送给老板

※而按照Async的方式,在输完数据以后,开始生成报表,但这个时候我们不干等这报表生成而是去写邮件,等报表明细生成以后,我们暂停邮件的编写去查看报表,确认以后继续写邮件知道发送完毕。

Asyncio的工作原理

明白了Sync和Async的套路,我们回到今天的主题,到底什么是Asyncio呢?

事实上,Asyncio和其他的Python程序一样,是单线程的,他只有一个主线程,但是恶意进行多个不同任务(task),这里的任务,就是特殊的future对象,这些不同的任务,被一个叫做event loop(事件循环)的对象控制。我可以把这里的任务,类比成多线程版本里的多个线程。

为了简化的了解这个问题,我们可以假设任务只有两个状态:一是预备状态;而是等待状态、预备状态是指任务目前空闲,但随时准备运行。而等待状态,是指已经运行,但正在等待外部的操作完成,比如I/O操作。

在这种情况下,事件循环会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务(具体选取那个任务,和其等待的时间长短、占用的资源等等相关),使其运行,一直到任务把控制权教会给事件循环为止。

值得一提的是,对于Asyncio来说,他的任务在运行时不会被外部的因素打断,因此Asyncio内的操作不会出现race condition的情况,这样就不需要我们担心线程安全的问题了。

Asyncio的用法

讲完了Asyncio的原理,我们结合具体的代码来看一下他的用法。还是以上一节课里下载网站上的内容为例,用Asyncio的写法如下(依旧是省略了异常处理)

import asyncio
import aiohttp
import time
async def download_one(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print('Read {} from {}.'.format(resp.content_length,url)) async def download_all(sites):
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*tasks) def main():
sites = [
'https://en.wikipedia.org/wiki/Portal:Arts',
'https://en.wikipedia.org/wiki/Portal:History',
'https://en.wikipedia.org/wiki/Portal:Society',
'https://en.wikipedia.org/wiki/Portal:Biography',
'https://en.wikipedia.org/wiki/Portal:Mathematics',
'https://en.wikipedia.org/wiki/Portal:Technology',
'https://en.wikipedia.org/wiki/Portal:Geography',
'https://en.wikipedia.org/wiki/Portal:Science',
'https://en.wikipedia.org/wiki/Computer_science',
'https://en.wikipedia.org/wiki/Python_(programming_language)',
'https://en.wikipedia.org/wiki/Java_(programming_language)',
'https://en.wikipedia.org/wiki/PHP',
'https://en.wikipedia.org/wiki/Node.js',
'https://en.wikipedia.org/wiki/The_C_Programming_Language',
'https://en.wikipedia.org/wiki/Go_(programming_language)'
] start_time = time.perf_counter()
asyncio.run(download_all(sites))
end_time = time.perf_counter()
print('Down {} sites in {} seconds'.format(len(sites),end_time-start_time)) if __name__ == '__main__':
main()

这里的Async和await关键字是Asyncio的最新的写法,表示这个语句/函数是non-blocked的,正好对应了前面讲的event loop的概念。如果任务执行的过程需要等待,则将其放入等待的列表中,然后继续执行状态列表里的任务。

主函数里的asyncio.run(coro)是Asyncio的root call,表示拿到event loop,运行输入的coro,直到他结束,最后关闭这个event loop。事实上,asyncio.run()是Python3.7+以后才引入的,相当于以前的版本中下面的语法

loop = asyncio.get_event_loop()
try:
loop.run_until_complete(coro)
finally:
loop.close()

至于Asyncio版本内的download_all(),和之前多线程版本也有很大的区别:

task = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)

这里的asynco.creat_task(core),表示对输入的协程coro创建一个任务,安排他的执行,并返回此任务对象。这个函数也是Python3.7以后的版本增加的,如果是之前的版本,我们可以用下面的方法代替:

asyncio.ensure_future(coro)

可以看到,这里我们对每一个网站的下载,都创建了一个对应的任务。

再往下看,asyncio.gather(*aws,loop=None,return_exception = False),则表示在事件循环中运行aws序列中所有的任务。当然,除了例子中用到的几个函数,Asyncio还提供了很多其他的用法,我们可以通过Python官方文档查看

最后我们可以通过最后的输出结果发现,这种方式的效率要比之前的多线程版本还要高一些,充分体现出其优势。

Asyncio的缺陷

通过前面的讲解我们可以看出Asyncio的强大,但是任何一种方案都不是完美无瑕的,都存在一定的局限性,当然Asyncio也同样如此。

在实际的工作中,要想用好Asyncio,特别是要发挥好其强大的功能,很多情况下必须要有相应的Python库作为支持,我们可能发现了在前面的多线程编程中我们都是用的request库,但是在这里我们用的是aiohttp库,原因就是request库是不兼容Asyncio的,而aiohttp库兼容。

Asyncio软件库的兼容性问题在Python3的早期一直是一个大问题,但是随着技术的发展,这个问题也在逐步得到解决。

另外,在使用Asyncio时,因为在任务的调度方面有了了更大的自主权,写代码就要更加注意,否则会很容易出错。

举个例子,如果我们需要await一系列的操作,就带使用asyncio.gathrer();如果是单个的Futures,或许使用asyncio.wait()就可以了。那么,对于一个future,我们是需要他run_until_complete()还是run_forever(),都是要好好思考一下的。诸如此类,都是我们在面对具体问题时需要考虑的。

多线程还是Asyncio?

我们已经把并发编程的两种方式都讲了,不过,遇到实际问题,我们选择那种编程方式呢?

总得来是,我们可以遵循下面的规范

※如果是I/O bound,并且I/O操作很慢,需要很多任务/线程协同实现,那么使用Asyncio更加合适

※如果是I/O bound,但是I/O操作很快,只需要有限数量的任务或线程,那么使用多线程就可以了

※如果是CPU bound,则需要多进程来提高运行效率。

总结

在今天的学习中,我们一起学习了Asyncio的原理和用法,比较了Asyncio和多线程各自的优缺点。

共同点:

都是并发操作,多线程同一时间点只有一个线程在运行,而协程是只有一个任务在执行;

不同点:多线程是在I/O阻塞的时候通过切换线程来达到并发的效果,什么时候切换是由操作系统决定的,开发者不用操心,但会造成race  condition;

    协程是只有一个线程,在I/O阻塞时候通过在线程内切换任务来达到并发的效果,在什么时候切换是由开发者决定的,不会有race condition的情况。

不同于多线程,Asyncio是单线程,但其内部event loop的训话机制,可以让他并发的运行多个不同的任务,并且比多线程享有更多的自主控制权。

Asyncio中的任务,在运行的过程中不会被打断,因此不会出现race condition的情况。尤其是咋I/O操作比较密集的时候 ,Asyncio的运行效率会更高,远比线程切换的损耗要小。并且Asyncio可以开启的任务数量也比多线程中的线程数量多。

但是要注意的是,很多情况下使用Asyncio需要特定的三方库的支持,,而如果I/O操作比较快并且不heavy,使用多线程也能有效的解决问题。

思考题

我们已经讲了两种并发编程的思路,也多次提到了并行编程(multi-processing),其适用于CPU heavy的场景,

现在的需求是输入一个列表,随便指定一个元素,求出从0到这个元素所有整数的平方和。下面是常规写法,如果有多进程版本,又要怎么写呢?

import time
def cpu_bound(number):
print(sum(i*i for i in range(number))) def calculate_sum(numbers):
for number in numbers:
cpu_bound(number) def main():
start_time = time.perf_counter()
numbers = [10000000 + x for x in range(20)]
calculate_sum(numbers)
end_time = time.perf_counter() print('Calculation takds {} seconds'.format(end_time-start_time)) if __name__ == '__main__':
main()

运行结果(只贴出来运行的总时长)

Calculation takds 20.637497200000002 seconds

在来看看这种方法

import time
import multiprocessing def cpu_bound(number):
return sum(i*i for i in range(number)) def find_sums(numbers):
with multiprocessing.Pool() as pool:
pool.map(cpu_bound,numbers) if __name__ == '__main__':
start_time = time.perf_counter()
numbers = [10000000 + x for x in range(20)]
find_sums(numbers)
end_time = time.perf_counter()
print('Calculation takds {} seconds'.format(end_time-start_time))

然后来看看最终的运行时间

Calculation takds 7.3418618 seconds

因为这里需要用大量的计算,所以使用的是多进程的方式来提高了程序的效率。

Python核心技术与实战——十八|Python并发编程之Asyncio的更多相关文章

  1. Python核心技术与实战——十六|Python协程

    我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...

  2. Python核心技术与实战——十四|Python中装饰器的使用

    我在以前的帖子里讲了装饰器的用法,这里我们来具体讲一讲Python中的装饰器,这里,我们从前面讲的函数,闭包为切入点,引出装饰器的概念.表达和基本使用方法.其次,我们结合一些实际工程中的例子,以便能再 ...

  3. Python核心技术与实战——十二|Python的比较与拷贝

    我们在前面已经接触到了很多Python对象比较的例子,例如这样的 a = b = a == b 或者是将一个对象进行拷贝 l1 = [,,,,] l2 = l1 l3 = list(l1) 那么现在试 ...

  4. python学习笔记(十八)网络编程之requests模块

    上篇博客中我们使用python自带的urllib模块去请求一个网站,或者接口,但是urllib模块太麻烦了,传参数的话,都得是bytes类型,返回数据也是bytes类型,还得解码,想直接把返回结果拿出 ...

  5. python并发编程之asyncio协程(三)

    协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...

  6. Python进阶:并发编程之Asyncio

    什么是Asyncio 多线程有诸多优点且应用广泛,但也存在一定的局限性: 比如,多线程运行过程容易被打断,因此有可能出现 race condition 的情况:再如,线程切换本身存在一定的损耗,线程数 ...

  7. Python核心技术与实战——十九|一起看看Python全局解释器锁GIL

    我们在前面的几节课里讲了Python的并发编程的特性,也了解了多线程编程.事实上,Python的多线程有一个非常重要的话题——GIL(Global Interpreter Lock).我们今天就来讲一 ...

  8. Python核心技术与实战——十五|深入了解迭代器和生成器

    我们在前面应该写过类似的代码 for i in [1,2,3,4,5]: print(i) for in 语句看起来很直观,很便于理解,比起C++或Java早起的 ; i<n;i++) prin ...

  9. Python核心技术与实战——十|面向对象的案例分析

    今天通过面向对象来对照一个案例分析一下,主要模拟敏捷开发过程中的迭代开发流程,巩固面向对象的程序设计思想. 我们从一个最简单的搜索做起,一步步的对其进行优化,首先我们要知道一个搜索引擎的构造:搜索器. ...

随机推荐

  1. Jmeter(一) - 调用数据的参数化

    1. 做性能测试, 不可避免的一点一定会有使用不同的用户密码进行登陆. 如何使登陆用户参数化呢?

  2. 【疑难杂症】new Date() 造成的线程阻塞问题

    代码如下 package com.learn.concurrent.probolem; import java.util.Date; import java.util.concurrent.Count ...

  3. JS点击img图片放大再次点击缩小JS实现 简单实用Ctrl+C+V就可以用

    业务需要,从后台获取的图片列表,用img标签展示,用户需要查看大图.记录下来以便学习和参考.示例图如下: 放大之前: 放大之后: 点击后放大(由于图片高度超出了页面,需要通过overflow:auto ...

  4. python学习之网络基础

    七 网络编程 7.1 C/S B/S架构 7.1.1 认识 Client : 客户端 Browser :浏览器 Server :服务器端 C/S:客户端与服务器之间的构架 B/S:浏览器与服务器之间的 ...

  5. DHCP迁移

    情况1:windows 2003迁移到windows 2003或者windows 2008,按照需要以下几个步骤:1.在源DHCP服务器导出DHCP数据文件,执行以下命令netsh dhcp serv ...

  6. 【Qt开发】在QLabel已经显示背景图片后绘制图形注意事项

    主要是要解决图形覆盖的问题,通常的办法就是对QLabel进行子类化,并重载函数: void myLabel::paintEvent(QPaintEvent *event)   {       QLab ...

  7. Java 浮点数的范围和精度

    本篇先介绍IEEE754标准中针对浮点数的规范,然后以问答形式补充有关浮点数的知识点. (一)IEEE754标准 IEEE 754 标准即IEEE浮点数算术标准,由美国电气电子工程师学会(IEEE)计 ...

  8. Centos(64位)安装Hbase详细步骤

    HBase是一个分布式的.面向列的开源数据库,该技术来源于 Fay Chang 所撰写的Google论文“Bigtable:一个结构化数据的分布式存储系统”.就像Bigtable利用了Google文件 ...

  9. Mac 如何将apache的这个默认目录更改到用户目录下

    如何将apache的这个默认目录更改到用户目录下. 做如下更改即可: 1.在自己的用户目录下新建一个Sites文件夹,我的用户目录为gaocuili 2.进到cd /etc/apache2/users ...

  10. Websocket --(3)实现

    今天介绍另外一种websocket实现方式,结合了spring MVC,并完善了第二节所提到做一个简单的登录认证用来识别用户的名称.界面继续沿用第二节的布局样式,同时增加上线和下线功能. 参考了 ht ...