任务和节之间的区别在于代码将执行的时间范围.节包含在sections
构造中,并且(除非指定了nowait
子句)线程在执行完所有节之前不会离开它:
[ sections ]
Thread 0: -------< section 1 >---->*------
Thread 1: -------< section 2 >*------
Thread 2: ------------------------>*------
... *
Thread N-1: ---------------------->*------
在这里,N
个线程遇到了一个包含两个部分的sections
构造,第二个部分比第一个部分花费更多的时间.前两个线程分别执行一个部分.其他N-2
个线程只是在节构造末尾的隐式屏障处等待(这里显示为*
).
只要有可能,任务就会在所谓的任务调度点排队并执行.在某些情况下,可以允许运行库在线程之间移动任务,即使是在线程生命周期的中段.这样的任务被称为未绑定的,并且未绑定的任务可能会在一个线程中开始执行,然后在某个调度点,它可能会被运行时迁移到另一个线程.
尽管如此,任务和部分在很多方面都是相似的.例如,以下两个代码片段实现了基本上相同的结果:
// sections
...
#pragma omp sections
{
#pragma omp section
foo();
#pragma omp section
bar();
}
...
// tasks
...
#pragma omp single nowait
{
#pragma omp task
foo();
#pragma omp task
bar();
}
#pragma omp taskwait
...
taskwait
的工作原理与barrier
非常相似,但对于任务来说——它确保当前的执行流将暂停,直到所有排队的任务都已执行.它是一个调度点,即允许线程处理任务.single
构造是必需的,因此任务只能由一个线程创建.如果没有single
构造,每个任务将被创建num_threads
次,这可能不是我们想要的.single
构造中的nowait
子句指示其他线程不要等到single
构造被执行(即移除single
构造末尾的隐式屏障).因此,他们立即达到taskwait
并开始处理任务.
为清楚起见,这里所示的taskwait
是一个明确的调度点.也有隐式的调度点,最明显的是在屏障同步内部,无论是显式的还是隐式的.因此,上面的代码也可以简单地写成:
// tasks
...
#pragma omp single
{
#pragma omp task
foo();
#pragma omp task
bar();
}
...
下面是一个可能的场景,如果有三个线程:
+--+-->[ task queue ]--+
| | |
| | +-----------+
| | |
Thread 0: --< single >-| v |-----
Thread 1: -------->|< foo() >|-----
Thread 2: -------->|< bar() >|-----
这里显示的| ... |
是调度点的动作(taskwait
指令或隐式屏障).基本上,线程1
和2
挂起它们在该点上正在做的事情,并开始处理队列中的任务.处理完所有任务后,线程将恢复正常的执行流.注意,线程1
和2
可能在线程0
退出single
构造之前到达调度点,因此不需要对齐左侧的|
(如上图所示).
甚至在其他线程能够请求任务之前,线程1
也可能能够完成对foo()
任务的处理并请求另一个任务.因此foo()
和bar()
可能由同一线程执行:
+--+-->[ task queue ]--+
| | |
| | +------------+
| | |
Thread 0: --< single >-| v |---
Thread 1: --------->|< foo() >< bar() >|---
Thread 2: --------------------->| |---
如果线程2来得太晚,那么单独的线程也可能执行第二个任务:
+--+-->[ task queue ]--+
| | |
| | +------------+
| | |
Thread 0: --< single >-| v < bar() >|---
Thread 1: --------->|< foo() > |---
Thread 2: ----------------->| |---
在某些情况下,编译器或OpenMP运行时甚至可能完全绕过任务队列,以串行方式执行任务:
Thread 0: --< single: foo(); bar() >*---
Thread 1: ------------------------->*---
Thread 2: ------------------------->*---
如果区域代码中没有任务调度点,OpenMP运行时可能会在它认为合适的时候启动任务.例如,有可能所有任务都被推迟到parallel
区域末尾的屏障.