实例Python并发编程

我们知道现在硬件飞速发展 , 多核CPU 成了标配 。为了提高程序的效率 , 一个方面改变程序的顺序执行 , 用异步方式 , 防止由于某个耗时步骤 , 而影响后续程序的执行 。另一个方面是采用并发方式执行 , 重复利用多核CPU优势加速执行 。关于并发编程大家可能比较熟悉的是Golang的协程、通道和Node.js 的async.parallel异步并发编程 。就并发编程来说 , Python不是一门合适的语言 , 主要是Python有一个解析器(CPython)内置的全局解释锁GIL 。GIL限制Python中一次只能有一个线程访问Python对象 , 从而我们无法实现多线程分配到多个CPU执行 , 这是一个极大限制 , 限制Python并发编程 。当然限制归限制 , Python标准库中都已经引入了多进程和多线程库 , 所以Python并发程序相当简单 。
本文中 , 虫虫给大家实例介绍一下Python的并发编程

实例Python并发编程

文章插图
 
并发编程关于python并发编程 , 我们推荐优雅地创建并发程序三部曲:
首先 , 编写一个按顺序执行任务的脚本 。
其次 , 脚本中的执行程序(耗时任务)提取为一个执行函数 , 并使用map函数调用 。
最后 , 使用并发模块中的函数替换map即可 。
实例脚本该实例中 , 我们用到一个小的图片爬虫 , 使用urllib从Picsum网站下载20张图片 , 具体脚本程序如下:
import urllib.requestimport timeurl = 'https://picsum.photos/id/{}/200/300'args = [(n, url.format(n)) for n in range(20)]start = time.time()for pic_id, url in args: res = urllib.request.urlopen(url) pic = res.read() with open(f'./{pic_id}.jpg', 'wb') as f: f.write(pic) print(f'图片 {pic_id} 已经保存!')end = time.time()msg ='共耗时 {:.3f} 秒下载完成 。'print(msg.format(end-start)python pic_get.py 运行该脚本 , 结果如下:
图片 0 已经保存!图片 1 已经保存!图片 2 已经保存!...共耗时 26.694 秒下载完成 。下载共耗费不到半分钟 , 接着按照我们优雅的三部曲 , 改造这个脚本 。
使用Map改造脚本下面脚本中 , 我们将下载图片的代码打包到一个执行函数get_img中 。
import urllib.requestimport timedef get_img(pic_id, url): res = urllib.request.urlopen(url) pic = res.read() with open(f'test/{pic_id}.jpg', 'wb') as f: f.write(pic) print(f'图片 {pic_id} 已经保存!')def main(): url = 'https://picsum.photos/id/{}/200/300' pic_ids = [i for i in range(20)] ; urls=[(url.format(n)) for n in range(20)] start = time.time() for _ in map(get_img, pic_ids, urls): pass end = time.time() msg = '共耗时{:.3f}秒下载完成 。' print(msg.format(end-start))if __name__ == '__main__': main()上述脚本中 , 用map函数替换先前脚本中的for循环(黑体部分) 。map是一个函数式编程语法 , 该函数会生成一个迭代器 , 迭代器会执行迭代调用get_img() 。关于map()函数熟悉函数式编程人可能会觉得有点奇怪 , 请自己搜索资料充电 , 此处 , 我们用它来充当并发编程网关 。
图片 0 已经保存!图片 1 已经保存!图片 2 已经保存!...图片 19 已经保存!共耗时26.023秒下载完成 。用map改造后 , 运行脚本总耗时大体上和脚本一致 。
多线程并发处理Python标准库的current.futures模块包含了大量并发编程的包装函数 , 详细说明 , 可参见官方文档 , 此处我们直接上代码 。
将pic_get1.py中的程序做简单改进 , 就能实现多线程脚本:
首先在脚本开头引入多线程函数:
from concurrent.futures import ThreadPoolExecutor接着替换
for _ in map(get_img, pic_ids, urls): pass为
with ThreadPoolExecutor(max_workers=20) as do: do.map(get_img, pic_ids, urls)即可 。执行结果:
图片 0 已经保存!图片 2 已经保存!图片 5 已经保存!...图片 9 已经保存!共耗时2.913秒下载完成 。总耗时由26秒 , 减少到了大约3秒 。大概快了8倍 。并发执行的效果还是杠杠的 。
程序中我们使用with ThreadPoolExecutor语句产生一个执行器do 。通过将get_img和相应的参数映射到执行程序 , 自动生成多线程执行 。
大家可能注意到了在多线程脚本执行后 , 图片下载时候不是以前的0~19的顺序的 , 而是不同线程并发执行的所以完成提示信息也是乱序的 。


推荐阅读