多线程编程

本文探讨了如何在C++、Java和Python中使用OpenMP、ExecutorService、ParallelStream等技术进行代码并行优化,包括矩阵求和任务的并行计算、线程池和进程池的使用,以及针对GIL限制的Python多进程策略。还涉及了Amdahl's law和并行策略选择的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

3.代码并行优化

在C++中,推荐使用OpenMP进行多线程计算使用时需要在CMakeLists.txt里面添加以下代码:

 

  •  
  •  
find_package(OpenMP REQUIRED)target_link_libraries(CodeCraft-2021 PUBLIC OpenMP::OpenMP_CXX)

👆可左右滑动查看完整代码

 

在矩阵求和例子中,我们可以把任务均分到每个线程上,每个线程负责计算矩阵的一部分的和,最后对所有线程求得的和进行汇总,代码如下:

 

// input: int n, int m, int mat[n][m]// output: int sum_mat#include <omp.h>int sums[2] = {0};#pragma omp parallel for num_threads(2)for (int i = 0; i < n; ++i) {    for (int j = 0; j < m; ++j) {        sums[omp_get_thread_num()] += mat[i][j];    }}int sum_mat = 0;for (int i = 0; i < 2; ++i) {    sum_mat += sums[i];}

 

#pragma omp parallel for 会将下一行的 for 循环自动分块并行化,其中 num_threads(2)为使用的线程数量,默认为机器的逻辑处理器数量(可用 omp_get_num_threads()方法获取),在这里使用 2 和线上保持一致。omp_get_thread_num()方法可获取当前运行该代码的线程号,范围为从 0 开始的自然数。 

 

 

在Java中,这里介绍两种方式进行多线程编程,ExecutorService和Parallel Stream。

 

ExecutorService建议使用newFixedThreadPool方法初始化一个大小为nThreads的线程池。代码如下:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// input: int n, int m, int[][] mat// output: int sumMatint nThreads = 2;ExecutorService executorService = Executors.newFixedThreadPool(nThreads);List<Future<Integer>> tasks = new ArrayList<>(n);for (int i = 0; i < n; ++i) {    int[] vec = mat[i];    tasks.add(executorService.submit(() -> {        int sum = 0;        for (int j = 0; j < m; ++k) {            sum += vec[j];        }        return sum;    }));}int sumMat = 0;for (Future<Integer> task : tasks) {    sumMat += task.get();}executorService.shutdown(); // 必需,否则程序无法正常结束

👆可左右滑动查看完整代码

 

在单个任务比较小的情况下(即m较小),由于并行产生的额外开销,上述代码反而比串行执行更慢。为了减少额外开销,我们可以将整个矩阵提前分块均分到每个线程上。代码如下:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// input: int n, int m, int[][] mat// output: int sumMatint nThreads = 2;ExecutorService executorService = Executors.newFixedThreadPool(nThreads);List<Future<Integer>> tasks = new ArrayList<>(nThreads);int block = (n + nThreads - 1) / nThreads;for (int i = 0; i < nThreads; ++i) {    int start = i * block;    int end = Math.min((i + 1) * block, m);    tasks.add(executorService.submit(() -> {        int sum = 0;        for (int j = start; j < end; ++j) {            for (int k = 0; k < m; ++k) {                sum += mat[j][k];            }        }        return sum;    }));}int sumMat = 0;for (Future<Integer> task : tasks) {    sumMat += task.get();}executorService.shutdown(); // 必需,否则程序无法正常结束

👆可左右滑动查看完整代码

 

Parallel Stream则是另外一个思路,将计算过程视为一个流(Stream),并用filter、map、sum、collect等方法将计算过程串起来,代码上会简洁很多,适用场景和ExecutorService不一样。该方法不能指定使用的线程数量。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// input: int n, int m, int[][] mat// output: int sumMatint sumMat = Stream.of(mat).parallel().mapToInt(vec -> {    int sumVec = 0;    for (int i = 0; i < m; ++i) {        sumVec += vec[i];    }    return sumVec;}).sum();

 

 

在Python中,由于GIL(Global Interpreter Lock)的存在,多线程编程不能完整利用上多个CPU核心,只能使用多进程并行。

 

为了让多个进程间共用同一份矩阵数据,可以将矩阵放在全局变量,在Linux下利用Copy-on-write的特性,避免矩阵的拷贝。代码如下:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
# input: n: Int, m: Int, mat: List[List[Int]] or numpy.ndarray# output: sum_mat: Intfrom multiprocessing import Pooldef sum_vec(i):    s = 0    for j in range(m):        s += mat[i][j]    return swith Pool(2) as p:    sum_mat = sum(p.map(sum_vec, range(n))) # 若将mat作为入参会引起序列化拷贝

👆可左右滑动查看完整代码

 

和上面Java的例子一样,可以提前对矩阵均分为2个部分均分给2个进程,减少额外开销,提高并行效率。

 

额外提醒,比赛为Python提供了CPython和PyPy两种解释器,前者推荐用numpy.ndarray实现上述的矩阵,并用numpy.sum实现求和,后者推荐直接用Python自带的列表类型实现矩阵。

 

不同的算法有着不一样的并行思路,下面将举两个细粒度不一样的并行思路。

 

01

 

在一些比较困难的问题上,最优解是很难直接求出来的,比如本次的赛题。在有限的时间内只能求出一个较优的解。在一些算法中,可以通过修改参数(包括随机种子)得到不一样的解,那么可以简单的用多个线程/进程同时运行不同参数的同个算法,将最终产生的不同的解取最优解。

 

02

 

在进行更细粒度的并行前,需要先确认程序的瓶颈位置。根据Amdahl’s law,将时间占比为p的瓶颈部分进行s倍提速后,整个程序将缩短为1/((1-p)+p/s)倍运行时间。确定瓶颈位置可以简单的用代码计时或是用相关语言的Profiler工具(推荐后者)。确定完瓶颈之后,找到相互独立的计算模块进行并行。

 

 

 

 

 

 

 

 

 

 

 

注意事项

 

 

 

 

 

   

👉在并行的程序中,不能通过CPU时间(如C++的clock())计算运行时间,而应该使用系统时间。

 

👉尽量避免线程/进程间的通信和同步。

   

👉不要盲目地进行并行。并行自身也会产生性能代价,根据不同的并行任务数、任务大小,并行的实际加速比差异很大,需要实际测量。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值