OpenMP 中的线程任务调度

简介: OpenMP中任务调度主要针对并行的for循环,当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代,则可能会造成各个线程计算负载的不平衡,影响程序的整体性能。 如下面的代码中,如果每个线程执行的任务数量平均分配,有的线程会结束早,有的线程结束晚: 1 #include...

OpenMP中任务调度主要针对并行的for循环,当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代,则可能会造成各个线程计算负载的不平衡,影响程序的整体性能。

如下面的代码中,如果每个线程执行的任务数量平均分配,有的线程会结束早,有的线程结束晚:

 1 #include<stdio.h>
 2 #include<omp.h>
 3 
 4 int main(){
 5     int a[100][100] = {0};
 6 #pragma omp parallel for
 7     for (int i =0; i < 100; i++){
 8         for(int j = i; j < 100; j++ )
 9             a[i][j] = ((i%7)*(j%13)%23);
10     }
11     return 0;
12 }

 

为此,OpenMP提供了schedule子句来实现任务的调度。

schedule子句:

  schedule(type[, size]),

  参数type是指调度的类型,可以取值为static,dynamic,guided,runtime四种值。其中runtime允许在运行时确定调度类型,因此实际调度策略只有前面三种。

  参数size表示每次调度的迭代数量,必须是整数。该参数是可选的。当type的值是runtime时,不能够使用该参数。

1.静态调度static

  大部分编译器在没有使用schedule子句的时候,默认是static调度。static在编译的时候就已经确定了,那些循环由哪些线程执行。

  当不使用size 时,将给每个线程分配┌N/t┐个迭代。当使用size时,将每次给线程分配size次迭代。

  如下面代码:

 1 #include<stdio.h>
 2 #include<omp.h>
 3 int main(){
 4     int a[100][100] = {0};
 5 #pragma omp parallel for schedule(static)
 6 //#pragma omp parallel for schedule(static,5)
 7     for (int i =0; i < 100; i++){
 8         printf("id=%d i=%d\n",omp_get_thread_num(),i);
 9     }
10     return 0;
11 }
View Code

  在四核机器上执行:

  (1)当不使用参数时,100/4=25,0-24由1号线程执行;25-49由2号线程执行;50-74由3号线程执行;75-99由4号线程执行

  (1)当不使用参数时,x(x=0,1,2,3)线程执行((n/5)%4)任务。其中n=0-99。

2.动态调度dynamic

  动态调度依赖于运行时的状态动态确定线程所执行的迭代,也就是线程执行完已经分配的任务后,会去领取还有的任务。由于线程启动和执行完的时间不确定,所以迭代被分配到哪个线程是无法事先知道的。

  当不使用size 时,是将迭代逐个地分配到各个线程。当使用size 时,逐个分配size个迭代给各个线程。

  如下面代码:

 1 #include<stdio.h>
 2 #include<omp.h>
 3 int main(){
 4     int a[100][100] = {0};
 5 #pragma omp parallel for schedule(dynamic)
 6 //#pragma omp parallel for schedule(dynamic,5)
 7     for (int i =0; i < 100; i++){
 8         printf("id=%d i=%d\n",omp_get_thread_num(),i);
 9     }
10     return 0;
11 }
View Code

3.启发式调度guided

   采用启发式调度方法进行调度,每次分配给线程迭代次数不同,开始比较大,以后逐渐减小。

  size表示每次分配的迭代次数的最小值,由于每次分配的迭代次数会逐渐减少,少到size时,将不再减少。如果不知道size的大小,那么默认size为1,即一直减少到1。具体采用哪一种启发式算法,需要参考具体的编译器和相关手册的信息。

 

三种运行方式总结:

静态调度static:每次哪些循环由那个线程执行时固定的,编译调试。由于每个线程的任务是固定的,但是可能有的循环任务执行快,有的慢,不能达到最优。

动态调度dynamic:根据线程的执行快慢,已经完成任务的线程会自动请求新的任务或者任务块,每次领取的任务块是固定的。

启发式调度guided:每个任务分配的任务是先大后小,指数下降。当有大量任务需要循环时,刚开始为线程分配大量任务,最后任务不多时,给每个线程少量任务,可以达到线程任务均衡。

相关文章
|
算法 Java Go
【多线程系列-03】深入理解java中线程的生命周期,任务调度
【多线程系列-03】深入理解java中线程的生命周期,任务调度
189 0
|
8月前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
74 0
|
Java 调度
ScheduledExecutorService:多线程任务调度
ScheduledExecutorService:多线程任务调度
793 0
ScheduledExecutorService:多线程任务调度
|
Java 调度
任务调度线程池
任务调度线程池
|
缓存 Java 调度
【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )
【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )
144 0
|
调度
任务调度(四)——ScheduledExecutorService替代Timer,实现多线程任务调度
       上篇博文《任务调度(三)——Timer的替代品ScheduledExecutorService简介》已经对ScheduledExecutorService做了简单介绍,其实使用ScheduledExecutorService来替代Timer也是迫不得已的事情。
1253 0
|
1月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
67 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
72 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
53 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
34 2