python threadpoolexecutor_Python并发初步

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

本文中,虫虫给大家实例介绍一下Python的并发编程

7bdaa09135ef22acb04b5aa2384c1fbc.png

并发编程

关于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的顺序的,而是不同线程并发执行的所以完成提示信息也是乱序的。

92c890b7c69fb6be1d946ea66b59f7b7.png

多进程处理

多进程的改造也非常简单,我么只需把之前多线程脚本中的ThreadPoolExecutor替换为ProcessPoolExecutor即可。

from concurrent.futures import ProcessPoolExecutor

...

with ProcessPoolExecutor(max_workers=20) as do: do.map(get_img, pic_ids, urls)

执行结果:

图片 9 已经保存!图片 6 已经保存!...图片 11 已经保存!图片 15 已经保存!共耗时4.606秒下载完成

也非常快了,4秒钟就完成了,但是比多线程的3秒,稍微慢点。为什么多进程要比多线程慢呢?顾名思义,多进程程序会启用多个进程,而多线程会使用线程。Python中一个进程可以运行多个线程。每个进程都有其适当的Python解释器和适当的GIL。相比较而已,启动一个进程是更加耗时,重的操作,所以需要花费的时间更多。

e12585272c63450cf3f5427d4c32db9a.png

斐波那契数列计算

13eaede22e4d24282fceddfb07861254.png

为了进一步说明Python中线程和进程之间的区别,我们再来举一个大量计算的例子,斐波那契数列的计算。

根据斐波那契数列的定义我们用递归方法编写实现其计算:

def fib(n): if n == 1: return 0 elif n == 2: return 1 else: return fib(n-1) + fib(n-2)

在不使用numpy的情况下用普通Python计算比较慢:

def main(): fib_range = list(range(1, 35)) times = [] for run in range(10): start = time.time() for n in fib_range: fib(n) end = time.time() times.append(end-start) print('波那契数列fib(35)计算平均耗时 {:.3f}。'.format(np.mean(times))

结果:

波那契数列fib(35)计算平均耗时 5.200

下面我们试着用并发计算来加速计算。

让我们通过线程加速它!为此,我用受信任的ThreadPoolExecutor替换for循环,如下所示:

with ProcessPoolExecutor() as do: do.map(fib, fib_range)

执行结果:

波那契数列fib(35)计算平均耗时 5.239。

什么?加速后,反而慢,好像多线程没起到作用。这就是GIL的因素导致的,尽管使用了多个线程,生成了一堆线程,但是这些线程都在同一进程中运行并共享一个GIL。所以斐波那契序列尽管是并发计算的,这些线程在只能在一个CPU上循序执行。

a3cbd8a405e0462cd04646870a691fb2.png

进程可以分布在不同的CPU核心,而在同一进程上运行的线程则不能。使CPU消耗最大的操作为CPU绑定操作。为了加快CPU限制的操作,应该启动多个进程计算。我们用ProcessPoolExecutor替换ThreadPoolExecutor再试试:

波那契数列fib(35)计算平均耗时 3.591

性能提高了一点。

除了并发的方式外,我们可以用算法优化方法来提高性能,在数值计算中,这是一种更有效的方法,比如,我们改造fib函数:

def fib(n): a, b, i = 0, 1, 1 while i < n: a, b = b, a + b i += 1 return b

上述方法中,巧妙用内存存中的变量历史迭代的前两次结果都存在内存中,所以该次计算中无需回溯迭代计算,这样计算效率O(1),基本上可以秒出结果。

使用新算法后的执行结果:

波那契数列fib(35)计算平均耗时 0.000。

总结

本文我们实例介绍了Python中的并发编程,关于并发编程由于标准库中给我们打包好了方便使用的并发函数使得其使用非常方便。需要注意的是Python中的并发不管是多线程在IO操作中是有效的,而在其他方面,如数值结算时候就受GIL限制无用了。关于并发计算和GIL有心的话,可以参考有关文档进一步深入学习了解。


http://www.niftyadmin.cn/n/1354777.html

相关文章

ole2高级编程技术 pdf_日本技术大牛力荐,《21天学通Python》帮你快速精通,pdf无偿送...

python的学习书籍小编看过很多&#xff0c;但是这本《21天学通python》真的是堪称极品&#xff01;本书的作者团队成员为一线开发工程师、资深编程专家或专业培训师&#xff0c;在编程开发方面有着丰富的经验&#xff0c;并已出版过多本相关畅销书&#xff0c;颇受广大读者认可…

JS获取对象的长度

常常会判断当对象中只有一个值的时候&#xff0c;提示用户不能再继续删除 js对象无法直接通过.length来获取对象的长度大小获取方式&#xff1a;var length Object.keys(obj).length;判断&#xff1a;if (Object.keys(this.form).length 1) {this.$message({ message: 不能删…

pip查看可安装包版本_pyhon3下pip安装使用教程(win10)

一、前言 pip 是 Python 包管理工具&#xff0c;该工具提供了对Python 包的查找、下载、安装、卸载的功能。官网下载比较慢,只有几k速度&#xff0c;大家如果还没下载python和pip可以到我的网盘下载。目前pip为v20.0.2版&#xff0c;pytho为v3.8.2&#xff0c;有最新版本会随时…

Property or method “form“ is not defined on the instance but referenced during

Property or method “form” is not defined on the instance but referenced during报错信息的处理 在data中定义一下 data{return{form:{}}}

layuiadmin上手好难_《Ninjala》评测:它真的比《喷射》做的好吗?

可能是目前Switch上优化最好的UE4游戏。尽管内容丰富&#xff0c;且有着自己别具一格的设计思路&#xff0c;但上手门槛过高&#xff0c;操作过于复杂&#xff0c;可能不会适合大多数玩家。优点对UE4引擎的优化到位&#xff0c;画面流畅丰富的游戏内容&#xff0c;具有深度的战…

properties文件_Spring Boot2 系列教程(四)理解配置文件 application.properties

在 Spring Boot 中&#xff0c;配置文件有两种不同的格式&#xff0c;一个是 properties &#xff0c;另一个是 yaml 。虽然 properties 文件比较常见&#xff0c;但是相对于 properties 而言&#xff0c;yaml 更加简洁明了&#xff0c;而且使用的场景也更多&#xff0c;很多开…