并行计算:利用多线程跑循环

本文介绍了在数据分析和计算密集型任务中如何利用多线程技术,如GNUParallel、xargs和R脚本中的foreach、doParallel以及Python的threading、ThreadPoolExecutor、multiprocessing和joblib库。文章详细展示了这些工具的用法,并对比了它们在并行处理中的优缺点和适用场景。
摘要由CSDN通过智能技术生成

在数据分析和计算密集型任务中,利用多线程运行循环非常重要,这种操作成为并行计算(Parallel Computing)。并行计算利用了多核处理器的能力,将复杂的计算任务分散到多个处理单元上同时执行,可以大幅度提高计算效率和节省宝贵的时间。随着现代计算机多核心处理器的普及,我们可以通过并行处理技术同时在多个核心上执行任务,相比于传统的单线程顺序执行,这种方法能更好地利用硬件资源。多线程特别适合于独立重复的任务,比如大数据集的处理,模型的多参数模拟,以及需要重复计算的情景。此外,多线程还可以改善用户体验,通过后台并行处理让前台应用保持响应状态,特别是在图形界面程序中。然而,多线程编程也引入了新的编程复杂性,如线程同步、数据共享等问题,因此需要谨慎设计和测试,以确保程序的正确性和效率。

1、Shell脚本多线程

1.1、利用GNU Parallel工具

用法1、从标准输入读取参数

① echo parameter1 parameter2 | tr ' ' '\n' | parallel -j NumOfJobs 'command {}'

     echo parameter1 parameter2 | tr ' ' '\n' | parallel -j NumOfJobs 'command1 {}; command2 {}'

② cat file | parallel -j NumOfJobs 'command {}'

     cat file | parallel -j NumOfJobs 'command1 {}; command2 {}'

     cat file1 file2 | parallel -j NumOfJobs 'command {}'

     cat file1 file2 | parallel -j NumOfJobs 'command1 {}; command2 {}'

用法2、从GNU parallel 的语法读入

① parallel -j NumOfJobs 'command {}' ::: parameter1 parameter2

    parallel -j NumOfJobs 'command1 {}; command2 {}' ::: parameter1 parameter2

② parallel -j NumOfJobs 'command {}' :::: file1 file2

     parallel -j NumOfJobs 'command1 {}; command2 {}' :::: file1 file2

echo -e 'bob\ntom\ntony\nlily' > 1.txt
echo -e 'BOB\nTOM\nTONY\nLILY' > 2.txt

## ① 标准输入读取参数

echo bob tom tony lily | tr ' ' '\n' | parallel -j 4 'echo my name is {}; echo {} is me.'

cat 1.txt 2.txt | parallel -j 4 'echo my name is {}; echo {} is me.'

## ② 从GNU parallel 的语法读入

parallel -j 4 'echo my name is {}; echo {} is me.' ::: bob tom tony lily
parallel -j 4 'echo my name is {}; echo {} is me.' :::: 1.txt 2.txt

这行代码使用 cat 和 parallel 命令,并包含了一些参数。下面是它们各自的作用:

 
  1. cat: 这是一个常用的Unix命令,用于读取文件并将其内容输出到标准输出。在这段代码中,cat 读取 /home/wyn05/metaGRS_Env_LF/P1_LDSC/sum/lung_function_dir.txt 文件并将其内容传递给管道。

  2. | (管道): 在UNIX和Linux中,管道连接两个命令,将前一个命令的标准输出作为后一个命令的标准输入。在您的代码中,它将 cat 命令的输出作为输入传递给了 parallel 命令。

  3. parallel: 这是GNU parallel命令,它用于以并行的方式执行从输入中读取到的命令。它可以显著提高多核心处理器上重复性任务的执行效率。

  4. -j 4: 这是 parallel 命令的参数。-j 后跟的数字表示 parallel 应该同时运行的作业(job)数,这里设置为 4,意味着 parallel 会同时执行最多四个进程。

  5. ' (单引号): 这表示传递给 parallel 命令的字符串开始和结束。此字符串中包含了实际被并行执行的命令,这些命令会替换 {} 符号为每次从文件 lung_function_dir.txt 中读取的行。命令中的具体内容并没有在你提供的代码片段中给出。

 

使用 parallel 的好处是可以利用多核处理器同时处理多个任务,从而提高执行效率。这个命令的后续部分将指定从输入文件中读取的每一行该如何被处理,并行地对每一行执行定义好的命令。

1.2、利用xargs结合-P参数

用法1、从标准输入读取参数

①cat file1 file2 | xargs -P NumOfJobs -n 1 -I {} bash -c 'command1 {}; command2 {}'

-P:作业数

-I:指定替换字符串的占位符,通常用{}表示

-n:指定每次传递给命令的参数为1个

bash -c:表示执行后面的命令字符串。通常,-c 选项允许在命令行上指定要执行的命令。

②echo parameter1 parameter2 | tr ' ' '\n' | xargs -P NumOfJobs -n 1 -I {} bash -c 'command1 {}; command2 {}'

用法2、-a指定文件

xargs -a file -I {} bash -c 'command1 {}; command2 {}'

echo -e 'bob\ntom\ntony\nlily' > 1.txt
echo -e 'BOB\nTOM\nTONY\nLILY' > 2.txt

## ① 标准输入读取参数

echo bob tom tony lily | tr ' ' '\n' | xargs -P 4 -I {} -n 1 bash -c 'echo my name is {}; echo {} is me.'

cat 1.txt 2.txt | xargs -P 4 -I {} -n 1 bash -c 'echo my name is {}; echo {} is me.'

## ② 从-a参数读取文件

xargs -a 1.txt -P 4 -I {} -n 1 bash -c 'echo my name is {}; echo {} is me.'

这段代码的目的是从一个文本文件中读取行,并为每一行行启动一个新的bash进程来执行一组命令,且这些进程是并行运行的。下面是每个组件的详细作用说明:

 
  1. cat: 这个命令将/home/wyn05/metaGRS_Env_LF/P1_LDSC/sum/lung_function_dir.txt文件的内容输出到标准输出(stdout),通常用于查看文本文件的内容。

  2. | (管道): 将cat命令输出的内容作为下一个命令的输入。它将cat的标准输出连接到xargs的标准输入。

  3. xargs: 一个用于构建和执行命令行的工具。它从标准输入读取数据,并将读入的数据作为参数传递给指定的命令。

  4. -I {}: 指定了替换字符串{}。该选项告知xargs,在子命令中遇到{}时,应替换为从输入中读取的文本。

  5. -P 4: 这个参数指导xargs并行执行最多4个进程。也就是说,xargs将同一时间最多运行4个子进程。

  6. -n 1: 这告诉xargs每次命令调用时只使用输入中的一行。与-I参数联合使用时,它确保每次调用的命令只接受一个参数。

  7. bash -c: 这是一个调用bash的命令来执行一个字符串中的命令序列。bash -c后面跟着的字符串被当作一系列命令在一个新的bash实例中执行。

 

在这段代码之后,如果继续写出完整的命令内容,bash -c将会执行{}占位符替换之后的每个命令序列,每个命令序列都将处理输入文件的一行数据。这全部在bash的一个新的实例中完成,在这种情况下,由于xargs的作用,这些命令会并行执行。

1.3、Parallel VS. Xagrs

parallel 和 xargs 之间的主要区别在于 parallel 是专门设计用于并行执行的,而 xargs 原本设计用于构建命令行来批量处理输入,它的并发能力是后来加上的一个功能。

功能性和灵活性:

  • GNU Parallel

    • 提供了更复杂的并发执行控制,比如延迟启动、作业完成执行和错误处理。
    • 可以并行运行不同的命令和不同的参数组合。
    • 支持重试失败的作业。
    • 可以保持输出的顺序,即使作业是并发完成的。
  • xargs

    • 相对简单,用于将输入转换为命令行参数。
    • 并发功能较为基础,只提供了一个指定并行作业数量的参数。
    • 没有内置的失败作业重试机制。
    • 输出可能会交织在一起,因为它并不保证输出的顺序。

使用场景:

 
  • GNU Parallel 更适合复杂的并行任务,特别是当你需要维持输出顺序或者需要对多个不同的命令并行执行时。
  • xargs 则适用于输入比较标准,命令比较简单的场景,尤其是在系统中没有安装GNU Parallel时的一个很好的替代品。

兼容性:

  • GNU Parallel 不是所有系统默认安装的工具,你可能需要手动安装。
  • xargs 几乎在所有的Linux发行版中都被默认安装。

学习曲线:

  • GNU Parallel 由于其丰富的功能,相对有更陡峭的学习曲线。
  • xargs 更简单易懂,对于不太复杂的并发需求来说,很容易上手。

总结来说,如果你只是需要简单的并行处理能力,xargs 就足够了。但如果你的需求更加复杂,例如需要维护输入和输出的顺序,或者控制并行任务之间的复杂依赖关系,那么 GNU Parallel 将是更好的选择。

2、R脚本多线程

2.1、使用foreach和doParallel包

install.packages("foreach")
install.packages("doParallel")

library(foreach)
library(doParallel)

# 定义文件名向量
names <- c("bob","tom","tony","lily")

# 设置并行后端,注册核心数
numCores <- detectCores() - 1  # 使用系统核心数减1
cl <- makeCluster(numCores)
registerDoParallel(cl)

# 使用 foreach 来并行遍历文件名
foreach(name = names) %dopar% {
  print(name)
  # 你的后续代码将替代这里的 print 语句完成实际的任务
  # 需要执行的操作
}

# 停止并行后端
stopCluster(cl)

在这段代码中,foreach 循环和 %dopar% 操作符用于在多个线程上并行处理每个文件。该循环会同时执行多个迭代,每个核心处理一次迭代。

 

请注意,print 语句在并行环境中不一定能如期输出到控制台,因为它可能会在并行工作节点上执行。如果你需要收集路径或其他结果,考虑将它们存储在列表中并返回,例如:

file_paths <- foreach(name = names, .combine = 'c') %dopar% {
      print(name)

}

2.2、使用parallel包的mclapply函数

# 载入parallel包
library(parallel)

# 定义文件名向量
names <- c("bob","tom","tony","lily")

# 生成并打印文件路径的函数
generate_and_print_path <- function(file_name) {
  file_path <- paste("C:/results/", name, sep = '')
  # 在并行处理中打印可能不会如预期工作;使用 return() 来捕获输出
  print(file_path)
  return(file_path)
}

# 确定使用的核心数
num_cores <- detectCores() - 1  # 常常留一个核心不用是个好习惯

# 使用mclapply并行处理文件名
file_paths <- mclapply(file_names, generate_and_print_path, mc.cores = num_cores)

# 显示结果
print(file_paths)

在使用mclapply时,你可以指定mc.cores参数来决定使用多少个CPU核心来执行计算。这可以显著加快处理时间,特别是在执行大量独立计算任务时。但需要注意的是,mclapply只能在支持fork系统调用的操作系统(如Unix/Linux)中运行,并且在Windows系统中不可用。如果你在Windows系统上工作,可以考虑使用parallel包中的其他函数,如parLapply等。

2.3、foreach VS. mclapply

使用foreachdoParallel的优势在于:

  • 它提供了一个比较灵活的并行循环结构。
  • 可以很容易地改变使用的并行后端,不仅限于本地计算资源。
  • foreach可以和多种并行计算包结合使用,如doMC(仅限于非Windows系统)、doSNOW等。
  • 通过.combine参数可以方便地合并结果

mclapply函数的优势在于:

  • 它是lapply函数的并行版本,使用起来非常直观,尤其是对那些已经习惯了使用apply系列函数的用户。
  • 适合在多核心机器上对列表形式的数据进行并行计算。

然而,mclapply函数主要有以下限制:

  • 它使用了fork来创建子进程,因此仅支持Unix-like系统,不支持Windows系统。
  • 并不适用于所有并行任务,比如分布式计算或在Windows系统上的并行计算。

总的来说,foreach搭配doParallel更加通用,灵活,而mclapply在使用上更加简单,方便。根据不同的需求场景选择更适合的并行工具就可以了。

3、python脚本多线程

3.1、使用 threading 模块

   import threading
   
   def my_function(i):
       # 执行相关任务
       print(f"Working on {i}")
   
   threads = []
   for i in range(5):
       t = threading.Thread(target=my_function, args=(i,))
       t.start()
       threads.append(t)
   
   for t in threads:
       t.join()

1、导入 Python 的 threading 模块,该模块用于创建和管理线程。

2、定义根据参数需要指定的操作函数

3、创建一个threads列表用于存储创建的线程对象

4、使用for循环创建线程并启动线程对象t,并加入thread列表中

5、使用for循环等待所有线程完成,这会使主线程等待直到线程 t 执行完成。确保了在主线程继续执行后面的代码之前,所有创建的线程都已经完成。

3.2、concurrent.futures 模块

   from concurrent.futures import ThreadPoolExecutor
   
   def my_function(i):
       # 执行相关任务
       print(f"Working on {i}")
   
   with ThreadPoolExecutor(max_workers=5) as executor:
       for i in range(5):
           executor.submit(my_function, i)

1、导入 ThreadPoolExecutor 类,这是一个线程池执行器,用于管理和执行多个线程任务。

2、定义根据参数需要指定的操作函数

3、使用ThreadPoolExecutor创建一个线程池,max_workers=5表示线程池中最多同时执行5个线程。使用with语句确保在使用完线程池后进行清理工作

4、通过executor.submit方法提交任务给线程池,my_function将会以参数i被调用

3.3、multiprocess.dummy 模块

   from multiprocessing.dummy import Pool as ThreadPool
   
   def my_function(i):
       # 执行相关任务
       print(f"Working on {i}")
   
   pool = ThreadPool(5) 
   results = pool.map(my_function, range(5))
   pool.close()
   pool.join()

1、导入Pool类并将其重命名为ThreadPool,这是一个用于创建线程池的类

2、定义根据参数需要指定的操作函数

3、创建一个包含5个线程的线程池

4、使用线程池的map方法,将my_function函数应用于range(5)中的每个元素

5、关闭线程池

6、等待线程池中的所有线程执行完毕。这会阻塞程序直到所有任务完成,然后才继续执行后面的代码。

3.4、joblib 库

   from joblib import Parallel, delayed
   
   def my_function(i):
       # 执行相关任务
       print(f"Working on {i}")
   
   # n_jobs=-1 表示使用所有可用的CPU核心
   Parallel(n_jobs=5)(delayed(my_function)(i) for i in range(5))

1、导入joblib库中的Paralleldelayed,这两个函数用于实现并行计算

2、定义根据参数需要指定的操作函数

3、Parallel(n_jobs=5):创建一个Parallel对象,其中n_jobs参数设置为5,表示并行执行时使用的CPU核心数为5

4、(delayed(my_function)(i) for i in range(5)):这是一个生成器表达式,用于生成一组函数调用。delayed(my_function)返回一个包装了my_function的延迟执行对象,然后通过(i for i in range(5))生成器表达式,将0到4的整数作为参数传递给my_function

5、Parallel(...)的调用:通过调用Parallel对象,以并行的方式执行生成器表达式中的函数调用。n_jobs=5指定了同时使用的CPU核心数。每个任务都是对my_function的调用,参数是从0到4的整数,因此会同时执行5个任务。

3.5、不同方法间比较

在Python中,"最好"的多线程方法取决于具体的应用场景和需求。以下是上述四种方法的比较,可以帮助你根据不同情况选择合适的方法:

  1. threading模块:

    • 优点:是Python标准库的一部分,不需要安装额外的库;提供了大量的线程相关的控制功能(如锁、事件等)。
    • 缺点:代码可能稍显复杂,尤其是在管理和同步多个线程时;受到全局解释器锁(GIL)的限制,在CPU密集型任务中可能不会提供性能上的提高。
  2. concurrent.futures.ThreadPoolExecutor

    • 优点:提供了一个简洁的API,让线程管理和任务提交变得容易;内置的Future对象让并行任务的结果和状态管理变得方便。
    • 缺点:同样受GIL限制,对于CPU密集型任务不适合;Python 3中的新功能,不可在Python 2中使用(除非额外安装futures库)。
  3. multiprocessing.dummy模块:

    • 优点:对于那些已经熟悉multiprocessing API的用户来说,可以很容易地切换到多线程实现;适用于I/O密集型任务。
    • 缺点:在进行线程同步和共享状态时,可能需要一些额外的工作;并不适合CPU密集型任务。
  4. joblib

    • 优点:特别适合于简单并行任务,特别是与数据处理和机器学习库(如scikit-learn)结合使用时;可以很方便地进行进程或线程之间的切换。
    • 缺点:是一个第三方库,需要额外安装;功能相对专一,多用于并行化计算密集型任务。

没有一种方法是适合所有场景的,适用性取决于任务的特点:

  • 对于I/O密集型任务(如文件读写、网络请求等),使用多线程可以提高性能,任何一种方法都可带来益处,但ThreadPoolExecutormultiprocessing.dummy可能更加方便。
  • 对于CPU密集型任务,由于GIL的存在,多线程可能不会提供性能提升;在这种情况下,使用多进程(如multiprocessing模块)可能是更佳的选择。
  • 如果对代码的简洁性和易用性有较高要求,concurrent.futuresjoblib将是较好的选择。
  • 如果需要详细控制线程行为或进行复杂的同步操作,threading模块可能会更符合需要。

基于以上比较,你可以根据实际场景和个人的编程偏好来选择适合的方法。

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QH_ShareHub

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值