在数据分析和计算密集型任务中,利用多线程运行循环非常重要,这种操作成为并行计算(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
命令,并包含了一些参数。下面是它们各自的作用:
cat
: 这是一个常用的Unix命令,用于读取文件并将其内容输出到标准输出。在这段代码中,cat
读取/home/wyn05/metaGRS_Env_LF/P1_LDSC/sum/lung_function_dir.txt
文件并将其内容传递给管道。
|
(管道): 在UNIX和Linux中,管道连接两个命令,将前一个命令的标准输出作为后一个命令的标准输入。在您的代码中,它将cat
命令的输出作为输入传递给了parallel
命令。
parallel
: 这是GNU parallel命令,它用于以并行的方式执行从输入中读取到的命令。它可以显著提高多核心处理器上重复性任务的执行效率。
-j 4
: 这是parallel
命令的参数。-j
后跟的数字表示parallel
应该同时运行的作业(job)数,这里设置为 4,意味着parallel
会同时执行最多四个进程。
'
(单引号): 这表示传递给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
进程来执行一组命令,且这些进程是并行运行的。下面是每个组件的详细作用说明:
cat
: 这个命令将/home/wyn05/metaGRS_Env_LF/P1_LDSC/sum/lung_function_dir.txt
文件的内容输出到标准输出(stdout),通常用于查看文本文件的内容。
|
(管道): 将cat
命令输出的内容作为下一个命令的输入。它将cat
的标准输出连接到xargs
的标准输入。
xargs
: 一个用于构建和执行命令行的工具。它从标准输入读取数据,并将读入的数据作为参数传递给指定的命令。
-I {}
: 指定了替换字符串{}
。该选项告知xargs
,在子命令中遇到{}
时,应替换为从输入中读取的文本。
-P 4
: 这个参数指导xargs
并行执行最多4个进程。也就是说,xargs
将同一时间最多运行4个子进程。
-n 1
: 这告诉xargs
每次命令调用时只使用输入中的一行。与-I
参数联合使用时,它确保每次调用的命令只接受一个参数。
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%
操作符用于在多个线程上并行处理每个文件。该循环会同时执行多个迭代,每个核心处理一次迭代。请注意,
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
使用
foreach
和doParallel
的优势在于:
- 它提供了一个比较灵活的并行循环结构。
- 可以很容易地改变使用的并行后端,不仅限于本地计算资源。
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
库中的Parallel
和delayed
,这两个函数用于实现并行计算2、定义根据参数需要指定的操作函数
3、
Parallel(n_jobs=5)
:创建一个Parallel
对象,其中n_jobs
参数设置为5,表示并行执行时使用的CPU核心数为54、
(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中,"最好"的多线程方法取决于具体的应用场景和需求。以下是上述四种方法的比较,可以帮助你根据不同情况选择合适的方法:
threading
模块:
- 优点:是Python标准库的一部分,不需要安装额外的库;提供了大量的线程相关的控制功能(如锁、事件等)。
- 缺点:代码可能稍显复杂,尤其是在管理和同步多个线程时;受到全局解释器锁(GIL)的限制,在CPU密集型任务中可能不会提供性能上的提高。
concurrent.futures.ThreadPoolExecutor
:
- 优点:提供了一个简洁的API,让线程管理和任务提交变得容易;内置的Future对象让并行任务的结果和状态管理变得方便。
- 缺点:同样受GIL限制,对于CPU密集型任务不适合;Python 3中的新功能,不可在Python 2中使用(除非额外安装futures库)。
multiprocessing.dummy
模块:
- 优点:对于那些已经熟悉
multiprocessing
API的用户来说,可以很容易地切换到多线程实现;适用于I/O密集型任务。- 缺点:在进行线程同步和共享状态时,可能需要一些额外的工作;并不适合CPU密集型任务。
joblib
:
- 优点:特别适合于简单并行任务,特别是与数据处理和机器学习库(如scikit-learn)结合使用时;可以很方便地进行进程或线程之间的切换。
- 缺点:是一个第三方库,需要额外安装;功能相对专一,多用于并行化计算密集型任务。
没有一种方法是适合所有场景的,适用性取决于任务的特点:
- 对于I/O密集型任务(如文件读写、网络请求等),使用多线程可以提高性能,任何一种方法都可带来益处,但
ThreadPoolExecutor
或multiprocessing.dummy
可能更加方便。- 对于CPU密集型任务,由于GIL的存在,多线程可能不会提供性能提升;在这种情况下,使用多进程(如
multiprocessing
模块)可能是更佳的选择。- 如果对代码的简洁性和易用性有较高要求,
concurrent.futures
或joblib
将是较好的选择。- 如果需要详细控制线程行为或进行复杂的同步操作,
threading
模块可能会更符合需要。基于以上比较,你可以根据实际场景和个人的编程偏好来选择适合的方法。