Python并发编程
正确合理地使用并发编程,无疑会给我们的程序带来极大的性能提升。今天我就带大家一起来剖析一下python的并发编程。这进入并发编程之前,我们首先需要先了解一下并发和并行的区别。
首先你需要知道,并发并不是指同一时刻有多个操作同时进行。相反,某个特定的时刻,它只允许有一个操作发生,只不过线程或任务之间会互相切换,直到完成。如下图所示:
图中出现了线程(thread) 和任务(task) 分别对应Python中两种并发形式--多线程(threading)和协程(asyncio)。对于多线程来说,是由操作系统来控制线程切换的。而对于 asyncio来说,主程序想要切换任务时,必须得到此任务可以被切换的通知。
对于并行来说,是指同一时刻、同时执行任务。如下图所示:
python中的多进程(multi-processing)是Python中的并行的实现形式。
对比来看,并发通常应用于I/O操作频繁的场景,而并行通常应用于CPU负载重的场景。
单线程与多线程性能比较
下面我们来比较一下单线程和多线程的性能区别。
我们先看一下单线程版本。
import time def process(work): time.sleep(2) print('process {}'.format(work)) def process_works(works): for work in works: process(work) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() process_works(works) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ##输出## process work1 process work2 process work3 process work4 use 8.016737222671509 seconds
单线程是最简单也是最直接的。
- 先是遍历任务列表;
- 然后对当前任务进行操作;
- 等到当前操作完成后,再对下一个任务进行同样的操作,一直到结束。
我们可以看到总共耗时约 8s。单线程的优点是简单明了,但是明显效率低下,因为上述程序的绝大多数时间,都浪费在了 I/O 等待上(假设time.sleep(2)是处理IO的时间)。下面我们来看一下多线程实现的版本。
import time import concurrent.futures def process(work): time.sleep(2) print('process {} '.format(work)) def process_works(works): with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.map(process, works) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() process_works(works) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ####输出#### process work1 process work2 process work3 process work4 use 2.006268262863159 seconds
可以看到耗时用了2s多,一下子效率提升了4倍。我们来分析一下下面这段代码。
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.map(process, works)
这里我们创建了一个线程池,总共有4个线程可以分配使用。excuter.map()表示对 works 中的每一个元素,并发地调用函数 process()。
并发编程之Asyncio
下面我们在来学习一下并发编程的另一种实现形式--Asyncio。Asyncio是单线程的,它只有一个主线程,但是可以运行多个不同的任务(task),这些不同的任务,被一个叫做 event loop 的对象所控制。你可以把这里的任务,类比成多线程版本里的线程。
为了简化讲解这个问题,我们可以假设任务只有两个状态:一是预备状态;二是等待状态。所谓的预备状态,是指任务目前空闲,但随时待命准备运行。而等待状态,是指任务已经运行,但正在等待外部的操作完成,比如 I/O 操作。在这种情况下,event loop 会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务,使其运行,一直到这个任务把控制权交还给 event loop 为止。当任务把控制权交还给 event loop 时,event loop会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成。如果完成,则将其放到预备状态的列表;如果未完成,则继续放在等待状态的列表。而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。这样,当所有任务被重新放置在合适的列表后,新一轮的循环又开始了:event loop 继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。
接下来我们看一下如何通过Asyncio来实现并发编程。
import asyncio import time async def process(work): await asyncio.sleep(2) print('process {}'.format(work)) async def process_works(works): tasks = [asyncio.create_task(process(work)) for work in works] await asyncio.gather(*tasks) def main(): works = [ 'work1', 'work2', 'work3', 'work4' ] start_time = time.time() asyncio.run(process_works(works)) end_time = time.time() print('use {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main() ####输出#### process work1 process work2 process work3 process work4 use 2.0058629512786865 seconds
到此为止,我们已经把python的两种并发编程方式多线程和Asyncio都讲完了。不过,遇到实际问题时,我们该如何进行选择呢?总的来说我们应该遵循以下规范。
- 如何I/O负载高,并且I/O操作很慢,需要很多任务/线程协同实现,那么使用 Asyncio 更合适。
- 如何I/O负载高,并且I/O操作很快,只需要有限数量的任务/线程,那么使用多线程就可以了。
欢迎大家留言和我交流。
了解更多有趣内容,获取更多资料,请关注公众号“程序员学长”,回复“资料”即可。