我希望了解这里的含义。为什么这个程序会“挂起”?
来自https://bisqwit.iki.fi/story/howto/openmp/
OpenMP和
fork()
值得一提的是,在调用fork()
的程序中使用OpenMP需要特别考虑。这个问题只影响GCC; ICC不受影响。如果您的程序打算使用daemonize()
或其他类似方法成为后台进程,则不得在fork之前使用OpenMP功能。在使用OpenMP功能之后,只有子进程不使用OpenMP功能时才允许使用fork,或者只允许使用fork作为一个全新的进程(例如在exec()
之后)。这是错误程序的一个例子:
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> void a(){ #pragma omp parallel num_threads(2) { puts("para_a"); // output twice } puts("a ended"); // output once } void b(){ #pragma omp parallel num_threads(2) { puts("para_b"); } puts("b ended"); } int main(){ a(); // Invokes OpenMP features (parent process) int p = fork(); if(!p){ b(); // ERROR: Uses OpenMP again, but in child process _exit(0); } wait(NULL); return 0; }
运行时,此程序挂起,永远不会到达输出“b结束”的行。目前没有解决方法,因为libgomp API没有指定可用于准备调用
fork()
的函数。
发布的代码违反了POSIX标准。
应使用单个线程创建进程。如果多线程进程调用fork(),则新进程应包含调用线程的副本及其整个地址空间,可能包括互斥锁和其他资源的状态。因此,为了避免错误,子进程可能只执行异步信号安全操作,直到调用其中一个
exec
函数为止。
运行OMP并行化代码显然违反了上述限制。
为了扩展Andrew Henle的答案,fork(2)
所做的是创建第二个进程,通过写时复制(CoW)内存映射共享调用线程的整个内存空间。子进程处于一种尴尬的境地 - 它是具有相同状态的父线程的副本(除了系统调用的返回值和其他一些东西,如定时器和资源使用计数器)以及对其所有内存和打开文件的访问权限描述符,但除了进行fork(2)
调用的那个之外没有任何其他执行线程。虽然有一些预防措施,但这可以用作多线程的粗略形式(并且在真正的LWP在Unix中引入之前用于此目的),99%的fork(2)
用于单一目的 - 产生子进程而子进程调用叉子后面的execve(2)
(或其标准C库中的一个前端)。认识到这一事实,有一个甚至更为极端的版本称为vfork(2)
甚至不创建父内存的CoW映射,而是直接使用其页表,有效地创建了独立进程和线程之间的混合。在这种情况下,子进程甚至不允许进行异步信号安全函数调用,因为它在父进程堆栈上运行。
请注意,OpenMP规范不包含与其他线程和/或进程控制机制的任何交互,因此,即使它可能适用于某些OpenMP实现,您的示例也不是正确的OpenMP程序。
这显然是死锁情景。它正好在下面的代码中发生。因为它在内部再次执行thread / fork()以并行执行puts("para_b");
。它有些被困在死锁中。
#pragma omp parallel num_threads(2)
{
puts("para_b");//in a trap means dead lock
}