前言
本文将和大家一起探讨python并发编程的实际运用,会以一些我实际使用的案例,或者一些典型案例来分享。本文使用的案例是我实际使用的案例(中篇),是基于之前效率不高的代码改写成并发编程的。让我们来看看改造的过程,这样就会对并发编程的高效率有个清晰地认知,也会在改造过程中学到一些知识。
本文为python并发编程的第十七篇,上一篇文章地址如下:
python:并发编程(十六)_Lion King的博客-CSDN博客
下一篇文章地址如下:
(暂无)
一、实施方案:使用多线程同时处理多个图片
多进程的处理速度我们已经了解了,如果基于相同代码的优化,那么多线程、多协程处理会怎么样呢?
1、使用10个线程直接测试40张图片的耗时
修改上一章版本的代码,只需要将进程池改为线程池即可,即ProcessPoolExecutor改为ThreadPoolExecutor,耗时大概为154秒(不用并发编程的情况下耗时 157 秒,在使用一个进程的情况下耗时192秒),并没有改善。
二、实施方案:使用多线程同时处理多个图片
1、使用10个协程直接测试40张图片的耗时
实验发现10个协程耗时大概为156秒(不用并发编程的情况下耗时 157 秒,在使用一个进程的情况下耗时192秒),并没有改善。这里装饰器没有监控到协程函数所用的时间,因此在代码里直接计算总体时间。
import os
from PIL import Image
import time
import concurrent.futures
import asyncio
def calculate_runtime(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
runtime = end_time - start_time
print(f"程序运行时间:{runtime}秒")
return result
return wrapper
@calculate_runtime
async def image_gray(img):
# 打开图片
img = Image.open(img)
# 计算平均灰度值
gray_sum = 0
count = 0
for x in range(img.width):
for y in range(img.height):
if img.mode == "RGB":
r, g, b = img.getpixel((x, y))
gray_sum += (r + g + b) / 3
elif img.mode == "L":
gray_value = img.getpixel((x, y))
gray_sum += gray_value
count += 1
avg_gray = gray_sum / count
return avg_gray
@calculate_runtime
def find_image(folder_path):
# 定义一个列表存储图片路径
images = []
# 遍历文件夹下的所有文件
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
# 处理每个文件,将其添加到列表中
images.append(file_path)
return images[0:40] # [0:40]
async def process_image(semaphore, img):
async with semaphore:
gray = await image_gray(img)
if gray < 50:
print(img, ":", gray)
@calculate_runtime
async def assert_run(folder_path):
images = find_image(folder_path)
# 创建一个事件循环
loop = asyncio.get_event_loop()
# 创建信号量控制并发数量
semaphore = asyncio.Semaphore(10)
# 创建任务列表
tasks = [process_image(semaphore, img) for img in images]
# 并发执行任务
await asyncio.gather(*tasks)
if __name__ == "__main__":
start_time = time.time()
folder_path = r'C:\Users\yeqinfang\Desktop\临时文件\文件存储'
asyncio.run(assert_run(folder_path))
end_time = time.time()
print("总耗时:", end_time-start_time)
三、关于多进程、多线程、多协程并发的探讨
上一章节我们已经了解到多进程带来了缩短时间的效果,而本章却看到了多线程、多协程几乎没啥 效果,以及实验中还有很多未解之谜,这些都将在本节一一解密。
1、为什么不是打开线程数越多,耗时越少?
上一章节中,无论使用10个、15个、20个还是60个进程,并发处理40张图片的耗时都相对稳定。这可能是由于以下原因:
CPU资源有限:如果系统的CPU资源有限,例如只有几个物理核心,无论启动多少个进程,实际上只有少数几个进程能够同时执行。这可能导致并发执行的进程数增加时,进程间的切换和调度开销变得更大,而实际的处理时间并没有显著改善。
其他因素的影响:除了进程数,还有许多其他因素可能影响程序的性能,例如磁盘IO、内存访问、操作系统调度等。这些因素可能导致并发执行的进程数增加时,性能并不线性增加。
为了更好地理解程序的性能瓶颈,你可以尝试对程序进行性能分析,例如使用Python的timeit
模块或专业的性能分析工具,来测量每个任务的执行时间、系统资源的利用情况等。这样可以帮助你确定是否存在性能瓶颈,并找到进一步优化的方向。
2、我的内核为6,是不是同一时间只有6个进程同时运行?
不完全正确,因为6个进程有可能在一个核上切换运行,因此,应该说同一时间最多只有6个进程同时运行。在一个具有6个物理内核的系统上,可以同时运行多个进程。然而,同时运行的进程数量不仅取决于物理内核的数量,还受到操作系统调度器的影响以及各个进程的资源需求和优先级等因素的影响。
操作系统的调度器负责管理进程之间的分配和切换,以实现并发执行。它可以根据各个进程的需求和优先级来进行调度和分配处理器时间。这意味着在一个6个物理内核的系统上,可以同时运行多个进程,每个进程在一个物理内核上执行。当然,调度器会根据需要在不同的内核之间进行切换,以实现多任务处理。
此外,还有其他因素会影响并发执行的进程数量,例如进程之间的依赖关系、资源利用率、IO操作的等待时间等。因此,并发执行的进程数量可能会超过物理内核数量,但是同时运行的进程数量受到物理内核数量的限制。
需要注意的是,一个6个物理内核的系统上,同时运行的进程数量可能会受到其他系统资源(如内存、硬盘等)的限制。因此,系统的性能和负载平衡也会对同时运行的进程数量产生影响。
3、我的电脑6核,但使用6个进程花费48秒,使用10个进程花费38秒,这是为什么呢?
只有6个物理核心,那么同时运行10个进程可能会导致一些进程被分配到同一个物理核心上执行(超过物理核心数量)。这可能会导致竞争和调度开销,从而影响性能。一般情况下,与物理核心数量相匹配的进程数会更有效地利用系统资源。使用10个进程比使用6个进程更快可能是由于以下原因:
(1)并发性:使用更多的进程可以增加并发性,充分利用系统的多个物理内核。当您的系统有6个物理内核时,使用6个进程时每个物理内核都可以独立执行一个进程。但当使用10个进程时,仍有4个进程可以被调度到其他空闲的物理内核上,这样可以更好地利用系统资源,提高处理效率。
(2)系统负载:使用更多的进程可以帮助平衡系统负载。在并发处理的情况下,某些任务可能会耗费较长时间,如果只有6个进程,这些耗时任务可能会占用物理内核的时间,导致其他任务等待。而使用10个进程可以将这些耗时任务分散到更多的物理内核上,避免了单一物理内核的过度负载,从而提高整体处理速度。
(3)任务特性:不同的任务可能具有不同的特性,某些任务可能更适合并行执行,而某些任务可能更适合顺序执行。如果您的任务是I/O密集型的(如读取/写入文件、网络请求等),那么使用更多的进程可能可以更好地利用I/O等待时间,从而提高整体效率。
综上所述,具体的性能差异可能受到多个因素的影响,包括任务特性、系统负载和资源利用等。在选择并发数量时,需要综合考虑这些因素,并进行测试和评估以确定最佳的配置。
需要注意的是,使用更多的进程并不总是意味着更快的速度。并发编程的性能受到多个因素的影响,包括任务的性质、数据交互、通信开销等。在您的具体情况下,10个进程可能在任务分配和执行的方式上更适合您的系统,从而导致更快的处理速度。但具体的性能表现还取决于任务的特点和系统的实际情况,因此仍然需要根据实际情况进行测试和调优。
4、为什么20个进程跟10个进程一样快?
当运行多个进程时,系统的调度和资源管理会发挥作用。虽然系统只有6个物理核心,但操作系统可以使用调度算法来分配进程的执行时间,并在不同的核心之间进行切换。
当运行10个进程时,操作系统可能会在6个物理核心上轮流调度这些进程,每个进程分配到一定的执行时间。这种情况下,由于存在调度开销和切换开销,可能会导致一些性能损失。
但当增加到20个进程时,操作系统可能会更频繁地进行进程切换和调度,以满足更多的进程需求。尽管这会增加一些调度和切换开销,但由于进程之间的任务可能是I/O密集型的,多个进程可以更好地利用I/O等待时间,从而提高整体效率。
另外,操作系统可能还有一些优化策略,例如使用多级反馈队列调度算法来优化进程调度,以使性能得到改善。
总的来说,性能的差异可能是由于系统调度和资源管理的复杂性以及进程间任务特性的不同造成的。不同的情况下可能会有不同的结果,所以在选择并发数量时,需要进行测试和评估以确定最佳的配置。
5、为什么60个进程比10个进程更慢?
有几个可能的原因导致60个进程比10个进程更慢:
(1)资源竞争:当并发进程数量增加时,每个进程都需要共享系统资源(如CPU时间、内存等),因此会产生更多的资源竞争。当并发进程过多时,系统可能无法有效地满足每个进程的资源需求,导致性能下降。
(2)上下文切换开销:进程之间的切换需要操作系统进行上下文切换,这涉及到保存和恢复进程的执行状态,这个过程需要一定的时间和开销。当并发进程数量增加时,上下文切换的次数也会增加,从而增加了系统的开销。
(3)系统调度策略:操作系统使用调度策略来决定进程的执行顺序和分配CPU时间。不同的调度策略可能会以不同的方式处理并发进程。当并发进程数量过多时,调度策略可能无法有效地平衡进程的执行,导致性能下降。
(4)系统资源限制:除了物理核心数量外,系统还有其他资源限制,如内存大小、磁盘带宽等。当并发进程数量增加时,可能会出现资源瓶颈,导致性能下降。
综上所述,性能差异可能是由于资源竞争、上下文切换开销、调度策略和系统资源限制等因素的综合影响。在选择并发进程数量时,需要进行测试和评估,找到适合系统配置的最佳数量。
6、为什么说并发执行的进程数量通常应该小于或等于物理内核的数量
并发执行的进程数量通常应该小于或等于物理内核的数量,这是因为每个物理内核都可以同时执行一个进程的指令。每个物理内核都有自己的处理器和一组寄存器,可以独立执行指令,并且可以并行处理多个指令流。
当并发执行的进程数量超过物理内核数量时,就会引发资源竞争和上下文切换开销。由于物理内核的数量是固定的,而进程数量的增加会导致进程之间频繁地进行上下文切换,这会增加系统的开销并降低性能。
另外,一些计算密集型任务可能会受到物理内核数量的限制。当进程数量超过物理内核数量时,系统会通过时间片轮转算法在不同的进程之间切换,每个进程只能获得有限的CPU时间片。这可能会导致每个进程的执行速度变慢,因为它们无法充分利用物理内核的计算能力。
因此,通常建议在并发编程中,将并发执行的进程数量控制在物理内核数量的范围内,以获得更好的性能和资源利用率。
7、处理图片灰度值,应该采用哪种并发编程方式?
实验已经发现,多进程优势明显,只是在考虑并发编程时,如处理图片灰度值,还是可以采用多种并发编程方式,具体选择哪种方式取决于应用的需求和环境的限制。以下是几种常见的并发编程方式可以考虑:
(1)多进程:可以使用multiprocessing
模块,将每张图片的处理任务分配给多个进程并行处理。由于每个进程有自己独立的内存空间,可以充分利用多核处理器的计算能力。
(2)多线程:可以使用threading
模块,将每张图片的处理任务分配给多个线程并行处理。由于线程共享同一进程的内存空间,因此对于计算密集型任务,多线程并发执行可能不如多进程效果好,但对于IO密集型任务可以有效利用等待时间。
(3)异步编程:可以使用asyncio
模块,将图片处理任务封装成协程函数,通过await
关键字实现异步执行。异步编程适用于IO密集型任务,可以充分利用等待时间,提高程序的效率。
具体选择哪种并发编程方式,需要综合考虑任务的特点、硬件资源、性能要求和编程复杂度等因素。在某些情况下,结合多种并发编程方式,例如多进程+多线程或多进程+异步编程,可以进一步提高性能和效率。
8、为什么处理图片灰度值,多进程比多线程耗时更少,效果更好?
处理图片灰度值时,多进程比多线程耗时更少、效果更好的主要原因是与GIL(全局解释器锁)相关。
在Python中,由于GIL的存在,同一时间只有一个线程能够执行Python字节码。这意味着在多线程情况下,即使有多个线程并发执行,但由于GIL的限制,实际上并不能同时利用多核处理器的计算能力。因此,对于CPU密集型任务(如计算灰度值),多线程并不能充分发挥多核处理器的优势,导致耗时较长。
而多进程则可以克服GIL的限制。每个进程都有自己独立的Python解释器和GIL,因此多个进程能够并行地利用多个CPU核心,同时执行任务。对于CPU密集型任务,多进程可以更好地利用多核处理器的计算能力,从而加快任务的处理速度。
此外,多进程在处理I/O密集型任务时也具有优势。由于I/O操作通常涉及等待时间,使用多进程可以充分利用这些等待时间,让其他进程继续执行任务,从而提高整体的并发性能。
需要注意的是,多进程的资源消耗会更大,包括更多的内存和进程间的通信开销。因此,在选择多进程还是多线程时,需要综合考虑任务类型、计算密集型还是I/O密集型、资源限制等因素,并根据具体情况选择最适合的并发编程方式。
9、为什么计算时间的装饰器没有监控到协程函数所用的时间?
协程函数的执行时间可能无法被装饰器直接监控到,这是由于协程函数的特性所导致的。协程函数在调用时会返回一个协程对象,而不是立即执行函数体内的代码。协程对象需要通过事件循环(event loop)来驱动执行。
由于装饰器在协程函数返回协程对象时被调用,而不是在协程函数执行时被调用,所以装饰器无法准确地测量协程函数的执行时间。
如果你希望测量协程函数的执行时间,可以在协程函数内部使用 time
模块的函数来手动记录开始和结束时间,并计算时间差。
10、一个脚本使用多进程执行代码后,为什么立即再次执行耗时比之前更久?
在使用多进程执行代码后,再次执行相同的代码可能会出现耗时更长的情况,这可能是由于以下原因导致的:
(1)进程资源占用:多进程会占用系统资源,包括CPU、内存等。如果之前的多进程执行过程中没有完全释放资源,再次执行时可能会导致资源竞争,从而导致执行时间增加。
(2)进程创建和销毁开销:每次创建进程都会有一定的开销,包括创建进程的时间和资源分配等。如果多次执行代码,每次都重新创建进程,这些额外的开销会累积导致耗时增加。
(3)其他系统因素:在多进程执行代码时,系统的负载可能会增加,特别是如果系统中有其他进程或任务正在运行。系统负载的增加可能会导致进程间的竞争,从而导致执行时间增加。
为了避免这种情况,可以尝试以下方法:
(1)确保在每次执行完多进程代码后,正确释放相关资源,包括关闭进程、清理临时文件等。
(2)考虑使用进程池(Process Pool)来管理进程的创建和销毁,这样可以避免频繁的进程创建和销毁带来的额外开销。
(3)在执行代码之前,确保系统的负载较低,可以暂停其他可能导致负载增加的任务或进程。
(4)考虑使用其他并发编程模型,如多线程或协程,根据实际情况选择合适的并发方式。
总的来说,多进程执行代码的耗时增加可能是由于资源竞争、进程创建销毁开销或系统负载等因素导致的。通过合理管理资源、减少额外开销以及调整系统负载,可以尽量避免耗时增加的情况。