使用多进程,求一个整数数组里的最大值

# 在做python头歌实验的时候遇到了新的知识点,遂记录

任务描述

本关任务:了解多线程和多进程的优缺点和应用场景。

相关知识

为了完成本关任务,你需要掌握:

  • 线程和进程的优缺点;
  • 线程和进程的应用场景。
多线程比多进程性能高?

误导!

应该说,多线程比多进程成本低,但性能更低。

在 UNIX 环境,多进程调度的开销和多线程调度的开销没有显著区别。就是说, UNIX 进程调度效率是很高的。内存消耗方面,二者只差全局数据区,现在内存都很便宜,服务器内存动辄若干 G,根本不是问题。

  • 多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车;

  • 多线程是平面交通系统,造价低,但红绿灯太多,老堵车。

我们现在都开跑车,油(主频)有的是,不怕上坡下坡,就怕堵车。

线程和进程的优缺点

我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。

多进程:

  多进程优点:

  • 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

  • 通过增加 CPU,就很容易扩充性能;

  • 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;

  • 每个子进程都有 2GB 地址空间和相关资源,总体能够达到的性能上限非常大。

  多进程缺点:

  • 逻辑控制复杂,需要和主程序交互;

  • 需要跨进程边界,如果有大量数据需要传送,就不太好,适合少量数据传送、密集运算,多进程调度开销比较大;

  • 最好是多进程和多线程结合,即根据实际的需要,每个 CPU 开启一个子进程,这个子进程开启多线程,可以为若干同类型的数据进行处理。当然你也可以利用多线程 + 多 CPU + 轮询方式来解决问题……;

  • 方法和手段是多样的,关键是自己看起来,实现方便又能够满足要求,代价也合适。

多线程:

  多线程的优点:

  • 无需跨进程边界;

  • 程序逻辑和控制方式简单;

  • 所有线程可以直接共享内存和变量等;

  • 线程方式消耗的总资源比进程方式好。

  多线程缺点:

  • 每个线程与主程序共用地址空间,受限于 2GB 地址空间;

  • 线程之间的同步和加锁控制比较麻烦;

  • 一个线程的崩溃可能影响到整个程序的稳定性;

  • 到达一定的线程数程度后,即使再增加 CPU 也无法提高性能,例如 Windows Server 2003,大约是 1500 个左右的线程数就快到极限了(线程堆栈设定为 1M ),如果设定线程堆栈为 2M ,还达不到 1500 个线程总数;

  • 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的 CPU。

    线程切换

无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?

我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这 5 科的作业,每项作业耗时 1 小时。

如果你先花 1 小时做语文作业,做完了,再花 1 小时做数学作业,这样,依次全部做完,一共花 5 小时,这种方式称为单任务模型,或者批处理任务模型。

假设你打算切换到多任务模型,可以先做 1 分钟语文,再切换到数学作业,做 1 分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核 CPU 执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写 5 科作业。

但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU 寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。

所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

线程和进程的应用场景

是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和 IO 密集型。

计算密集型任务的特点是要进行大量的计算,消耗 CPU 资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU 执行任务的效率就越低。所以,要最高效地利用 CPU,计算密集型任务同时进行的数量,应当等于 CPU 的核心数。

计算密集型任务由于主要消耗 CPU 资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用 C 语言编写。

第二种任务的类型是 IO 密集型,涉及到网络、磁盘 IO 的任务都是 IO 密集型任务。这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多,CPU 效率越高,但也有一个限度。常见的大部分任务都是 IO 密集型任务,比如 Web 应用。

IO 密集型任务执行期间,99% 的时间都花在 IO 上,花在 CPU 上的时间很少。因此,用运行速度极快的 C 语言并不能提升运行效率。对于 IO 密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C 语言最差。

编程要求

在右侧编辑器 Begin-End 区间补充代码,使用多线程或者多进程,求一个整数数组里的最大值。

测试说明

测试数据第一行一个整数n表示整数数组的长度,第二行n个整数表示数组里的每个元素,你只需要输出一个数字,表示数组里的最大值即可。

测试输入: 5 8 9 12 8 0 预期输出: 12

实验代码

import math
from multiprocessing import cpu_count
from multiprocessing import Pool

N = int(input())
a = list(map(int, input().split()))
# split() 方法:将输入的字符串分割成多个子字符串,通常是按空格分割。如果需要按其他分隔符分割,可以传递参数给 split() 方法
# map(int, ...) 函数:map 函数接受一个函数和一个可迭代对象作为参数。在这个例子中,它接受 int 函数和由 split() 返回的子字符串列表。map 函数会对列表中的每个元素执行 int 函数,即将每个字符串转换为整数
# list(...) 函数:将 map 函数返回的映射对象转换成列表

def howMany(T):
    ans = 0;
    for i in range(T[0] - 1, T[1]):
        ans = max(ans, a[i])
    return ans
# 对整个数字空间N进行分段CPU_COUNT
def seprateNum(N, CPU_COUNT):
    list = [[i * (N // CPU_COUNT) + 1, (i + 1) * (N // CPU_COUNT)] for i in range(0, CPU_COUNT)]
    list[0][0] = 1
    if list[CPU_COUNT - 1][1] < N:
        list[CPU_COUNT - 1][1] = N
    return list
    
    
if __name__ == '__main__':
# 多进程
#********** Begin *********#
    CPU_COUNT = cpu_count()
    ranges = seprateNum(N, CPU_COUNT)
    
    # with ... as p: 这是一个上下文管理器(context manager)的用法,确保在使用完进程池后,自动关闭并清理进程池。上下文管理器的好处是,它可以自动处理资源的分配和释放,避免手动关闭进程池。
    with Pool(CPU_COUNT) as p: # 使用 Pool 创建一个进程池,并行计算每个分段的最大值
        results = p.map(howMany, ranges) # p.map: map 方法将一个函数应用到一个可迭代对象的每一个元素上,这里是将 howMany 函数应用到 ranges 列表的每一个元素上。map 方法会将任务分配给进程池中的多个进程并行执行。
# map 方法会返回一个列表 results,其中包含每个区间的最大值。这些最大值是各个进程计算出来的结果,map 方法会在所有进程完成任务后收集这些结果并返回

# howMany: 这是我们定义的函数,用于计算数组指定区间内的最大值。它接收一个区间范围作为参数,并返回该区间内的最大值。
# ranges: 这是一个列表,其中每个元素都是一个区间(子数组)的起始和结束索引。这个列表将整个数组分割成多个区间,每个区间由一个进程处理。

# 假设 CPU_COUNT 为 4,ranges 为 [[1, 2], [3, 4], [5, 5]],数组 a 为 [8, 9, 12, 8, 0],则:

# 第一个进程处理范围 [1, 2],即数组的前两个元素 [8, 9],找到最大值 9。
# 第二个进程处理范围 [3, 4],即数组的第三和第四个元素 [12, 8],找到最大值 12。
# 第三个进程处理范围 [5, 5],即数组的最后一个元素 [0],找到最大值 0。
# 最终 results 列表为 [9, 12, 0],主进程对 results 列表取最大值,结果为 12。
        
    print(max(results))
#********** End *********#

代码解析

  1. 导入必要模块:

    • math, cpu_count, 和 Pool 来自 multiprocessing 模块。
  2. 读取输入:

    • 使用 input() 读取输入数据并转换为整数数组。
  3. 函数 howMany:

    • 这个函数计算给定范围内的最大值。
    • 输入是一个列表 [start, end],表示数组的一个子区间。
  4. 函数 seprateNum:

    • 根据CPU数量将数组分成若干个区间,每个区间的长度尽量相等。
    • 返回一个包含多个区间范围的列表。
  5. 主程序:

    • 确定CPU核心数 CPU_COUNT
    • 调用 seprateNum 函数将数组分段。
    • 使用 Pool 创建一个进程池,并并行计算每个分段的最大值。
    • 最终在主进程中计算并输出这些结果中的最大值。

运行说明

  1. 输入:

    • 第一行是整数 n,表示数组的长度。
    • 第二行是 n 个整数,表示数组中的元素。
  2. 输出:

    • 一个整数,表示数组中的最大值。

代码片段解析

with Pool(CPU_COUNT) as p:
    results = p.map(howMany, ranges)
1. with Pool(CPU_COUNT) as p:
  • Pool: 这是 multiprocessing 模块中的一个类,用于创建一个工作进程池。进程池允许你并行执行任务。
  • CPU_COUNT: 这是传递给 Pool 的参数,表示要创建的进程数量。我们使用 cpu_count() 函数来获取当前系统的CPU核心数,确保我们创建的进程数与可用的CPU核心数一致,以最大化并行执行效率。
  • with ... as p: 这是一个上下文管理器(context manager)的用法,确保在使用完进程池后,自动关闭并清理进程池。上下文管理器的好处是,它可以自动处理资源的分配和释放,避免手动关闭进程池。
2. results = p.map(howMany, ranges)
  • p.map: map 方法将一个函数应用到一个可迭代对象的每一个元素上,这里是将 howMany 函数应用到 ranges 列表的每一个元素上。map 方法会将任务分配给进程池中的多个进程并行执行。
  • howMany: 这是我们定义的函数,用于计算数组指定区间内的最大值。它接收一个区间范围作为参数,并返回该区间内的最大值。
  • ranges: 这是一个列表,其中每个元素都是一个区间(子数组)的起始和结束索引。这个列表将整个数组分割成多个区间,每个区间由一个进程处理。

详细执行流程

  1. 创建进程池:

    • Pool(CPU_COUNT) 创建一个包含 CPU_COUNT 个进程的进程池。进程池管理这些进程并将任务分配给它们。
  2. 将函数应用到数据:

    • p.map(howMany, ranges) 会将 ranges 列表中的每一个元素(即一个区间范围)传递给 howMany 函数,并将每个区间的最大值计算出来。map 方法会自动将这些计算任务分配给进程池中的多个进程并行处理。
  3. 获取结果:

    • map 方法会返回一个列表 results,其中包含每个区间的最大值。这些最大值是各个进程计算出来的结果,map 方法会在所有进程完成任务后收集这些结果并返回。
  4. 自动关闭进程池:

    • with 语句块结束时,进程池 p 会被自动关闭。上下文管理器确保资源被正确释放,避免资源泄漏。

示例

假设 CPU_COUNT 为 4,ranges[[1, 2], [3, 4], [5, 5]],数组 a[8, 9, 12, 8, 0],则:

  1. 第一个进程处理范围 [1, 2],即数组的前两个元素 [8, 9],找到最大值 9
  2. 第二个进程处理范围 [3, 4],即数组的第三和第四个元素 [12, 8],找到最大值 12
  3. 第三个进程处理范围 [5, 5],即数组的最后一个元素 [0],找到最大值 0

最终 results 列表为 [9, 12, 0],主进程对 results 列表取最大值,结果为 12

多进程优势

使用多进程的优势在于可以充分利用多核CPU的并行计算能力,特别是对于大数据集时,能够显著提高计算速度。通过将任务分割并行处理,各个进程同时计算部分数据的最大值,最终汇总结果,从而达到加速计算的目的。

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值