百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

实例Python并发编程(python并发原理)

off999 2024-10-23 12:40 24 浏览 0 评论

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

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

并发编程

关于python并发编程,我们推荐优雅地创建并发程序三部曲:

首先,编写一个按顺序执行任务的脚本。

其次,脚本中的执行程序(耗时任务)提取为一个执行函数,并使用map函数调用。

最后,使用并发模块中的函数替换map即可。

实例脚本

该实例中,我们用到一个小的图片爬虫,使用urllib从Picsum网站下载20张图片,具体脚本程序如下:

import urllib.request
import time
url = '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.request
import time
def 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的顺序的,而是不同线程并发执行的所以完成提示信息也是乱序的。

多进程处理

多进程的改造也非常简单,我么只需把之前多线程脚本中的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。相比较而已,启动一个进程是更加耗时,重的操作,所以需要花费的时间更多。

斐波那契数列计算

为了进一步说明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上循序执行。

进程可以分布在不同的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有心的话,可以参考有关文档进一步深入学习了解。

相关推荐

python入门到脱坑经典案例—清空列表

在Python中,清空列表是一个基础但重要的操作。clear()方法是最直接的方式,但还有其他方法也可以实现相同效果。以下是详细说明:1.使用clear()方法(Python3.3+推荐)...

python中元组,列表,字典,集合删除项目方式的归纳

九三,君子终日乾乾,夕惕若,厉无咎。在使用python过程中会经常遇到这四种集合数据类型,今天就对这四种集合数据类型中删除项目的操作做个总结性的归纳。列表(List)是一种有序和可更改的集合。允许重复...

Linux 下海量文件删除方法效率对比,最慢的竟然是 rm

Linux下海量文件删除方法效率对比,本次参赛选手一共6位,分别是:rm、find、findwithdelete、rsync、Python、Perl.首先建立50万个文件$testfor...

数据结构与算法——链式存储(链表)的插入及删除,

持续分享嵌入式技术,操作系统,算法,c语言/python等,欢迎小友关注支持上篇文章我们讲述了链表的基本概念及一些查找遍历的方法,本篇我们主要将一下链表的插入删除操作,以及采用堆栈方式如何创建链表。链...

Python自动化:openpyxl写入数据,插入删除行列等基础操作

importopenpyxlwb=openpyxl.load_workbook("example1.xlsx")sh=wb['Sheet1']写入数据#...

在Linux下软件的安装与卸载(linux里的程序的安装与卸载命令)

通过apt安装/协助软件apt是AdvancedPackagingTool,是Linux下的一款安装包管理工具可以在终端中方便的安装/卸载/更新软件包命令使用格式:安装软件:sudoapt...

Python 批量卸载关联包 pip-autoremove

pip工具在安装扩展包的时候会自动安装依赖的关联包,但是卸载时只删除单个包,无法卸载关联的包。pip-autoremove就是为了解决卸载关联包的问题。安装方法通过下面的命令安装:pipinsta...

用Python在Word文档中插入和删除文本框

在当今自动化办公需求日益增长的背景下,通过编程手段动态管理Word文档中的文本框元素已成为提升工作效率的关键技术路径。文本框作为文档排版中灵活的内容容器,既能承载多模态信息(如文字、图像),又可实现独...

Python 从列表中删除值的多种实用方法详解

#Python从列表中删除值的多种实用方法详解在Python编程中,列表(List)是一种常用的数据结构,具有动态可变的特性。当我们需要从列表中删除元素时,根据不同的场景(如按值删除、按索引删除、...

Python 中的前缀删除操作全指南(python删除前导0)

1.字符串前缀删除1.1使用内置方法Python提供了几种内置方法来处理字符串前缀的删除:#1.使用removeprefix()方法(Python3.9+)text="...

每天学点Python知识:如何删除空白

在Python中,删除空白可以分为几种不同的情况,常见的是针对字符串或列表中空白字符的处理。一、删除字符串中的空白1.删除字符串两端的空白(空格、\t、\n等)使用.strip()方法:s...

Linux系统自带Python2&amp;yum的卸载及重装

写在前面事情的起因是我昨天在测试Linux安装Python3的shell脚本时,需要卸载Python3重新安装一遍。但是通过如下命令卸载python3时,少写了个3,不小心将系统自带的python2也...

如何使用Python将多个excel文件数据快速汇总?

在数据分析和处理的过程中,Excel文件是我们经常会遇到的数据格式之一。本文将通过一个具体的示例,展示如何使用Python和Pandas库来读取、合并和处理多个Excel文件的数据,并最终生成一个包含...

【第三弹】用Python实现Excel的vlookup功能

今天继续用pandas实现Excel的vlookup功能,假设我们的2个表长成这样:我们希望把Sheet2的部门匹在Sheet1的最后一列。话不多说,先上代码:importpandasaspd...

python中pandas读取excel单列及连续多列数据

案例:想获取test.xls中C列、H列以后(当H列后列数未知时)的所有数据。importpandasaspdfile_name=r'D:\test.xls'#表格绝对...

取消回复欢迎 发表评论: