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操作很快,只需要有限数量的任务/线程,那么使用多线程就可以了。

欢迎大家留言和我交流。