【问题标题】:Understanding OpenMP shortcomings regarding fork了解有关 fork 的 OpenMP 缺点
【发布时间】:2018-03-01 12:04:54
【问题描述】:

我想明白他们在这里的意思。为什么这个程序会“挂起”?

来自https://bisqwit.iki.fi/story/howto/openmp/

OpenMP 和fork() 值得一提的是,在一个 调用fork() 的程序需要特别考虑。这 问题只影响 GCC; ICC 不受影响。如果你的程序 打算成为使用daemonize()或其他的后台进程 类似的意思是,你一定不能在fork 之前使用OpenMP 功能。 使用 OpenMP 功能后,只有在以下情况下才允许分叉 子进程不使用 OpenMP 功能,或者它作为一个 全新的流程(如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() 的函数。

【问题讨论】:

  • 这是执行两次puts("para_b");
  • 很可能这与分叉克隆只是当前线程的事实有关,而 OpenMP 期望它的线程池在那里,准备好执行东西。在第一次并行循环调用时,它将启动线程池并设置一些它正在运行的标志,并且在b 中,它只会将工作移交给线程池 - 但是没有任何线程支持,因为它们不存在于新流程中。一般来说,“thread”和“fork”模型很难协调,通常你只需要选择一个。
  • 这是已知的。请参阅libogmp 错误报告:gcc.gnu.org/bugzilla/show_bug.cgi?id=52303 cmets 中的注释:“POSIX 仅列出了一些您可以在 fork 之后调用的函数,然后再调用 _exit 或 exec*。#pragma omp parallel 绝对不是您可以在 fork 之后执行的操作来自多线程程序。”

标签: c openmp


【解决方案1】:

发布的代码违反了 POSIX 标准。

POSIX fork() standard states:

应使用单个线程创建进程。如果是多线程 进程调用 fork(),新进程应包含 调用线程及其整个地址空间,可能包括 互斥锁和其他资源的状态。 因此,要避免 错误,子进程只能执行异步信号安全 操作直到调用 exec 函数之一。

运行 OMP 并行代码显然违反了上述限制。

【讨论】:

    【解决方案2】:

    为了扩展 Andrew Henle 的回答,fork(2) 所做的是创建第二个进程,该进程通过写时复制 (CoW) 内存映射共享调用线程的整个内存空间。子进程处于尴尬的境地——它是父线程的副本,具有相同的状态(除了系统调用的返回值和其他一些东西,如计时器和资源使用计数器)并访问其所有内存和打开文件描述符,但除了进行fork(2) 调用的线程之外没有任何其他执行线程。虽然通过一些预防措施,这可以用作多线程的粗略形式(并且在 Unix 中引入真正的 LWP 之前它就被用于此目的),99% 的情况 fork(2) 服务于一个单一的目的 - 产生子进程,而child 在 fork 之后立即调用 execve(2)(或标准 C 库中的前端之一)。认识到这一点,有一个更极端的版本,称为vfork(2),它甚至不创建父内存的 CoW 映射,而是直接使用其页表,有效地创建了独立进程和线程之间的混合。在这种情况下,甚至不允许子级进行异步信号安全函数调用,因为它在父级的堆栈上进行操作。

    请注意,OpenMP 规范不涵盖与其他线程和/或进程控制机制的任何交互,因此,即使它可能适用于某些 OpenMP 实现,您的示例也不是正确的 OpenMP 程序。

    【讨论】:

      【解决方案3】:

      我在以下场景中遇到了这个问题:

      • 使用 Python
      • 实现 C++ python 扩展
      • 在此扩展程序中使用 OpenMP
      • 使用 Python 多处理并行执行(似乎使用 fork 和 copy-on-write 在子进程之间提供“免费的”穷人只读共享内存)。
      • 在子流程中调用扩展 => 死锁。

      (使用 Python 多线程不起作用,因为每个并行任务中的代码都有大量 Python 代码,然后由于 GIL,这些代码基本上变成了单线程。出于同样的原因,仅仅以串行方式运行代码,并且仅受益于 C++ 扩展内部的并行化。)

      请注意,调用诸如 numpy corrcoef 之类的并行函数确实设法在每个子进程中使用并行处理。大概它不使用 OpenMP 来做到这一点。

      OpenMP 中的“本来很好”有一个“重置所有内容,忘记所有以前生成的线程,就好像我们刚刚开始执行一样”。然后我们可以在分叉的子进程上调用这个函数并在每个子进程中使用 OpenMP(注意减少使用的 OpenMP 线程的数量,以免系统过载)。

      【讨论】:

        【解决方案4】:

        这显然是一个死锁场景。它发生在下面的代码中。因为它在内部再次实现 thread/fork() 以并行执行puts("para_b");。它有些陷入死锁。

         #pragma omp parallel num_threads(2)
         {
           puts("para_b");//in a trap means dead lock
         }
        

        【讨论】:

        • 使用互斥锁试试看。别猜了。
        • 编译过程中#pragma omp parallel num_threads(2)如何替换puts("para_b");这一行不清楚。因此建议使用互斥锁。是的,互斥锁不会解决这个问题。所以修改了我的答案。谢谢
        • 其实挺清楚的。只需使用 -fdump-tree-all 参数运行 GCC 并检查 OpenMP 降低步骤的输出。并行区域的主体在一个单独的函数中概述,然后由主线程直接调用或由工作线程中的 GOMP 间接调用。不会对块的内容进行任何更改。
        • 阻塞发生在 GOMP 代码中,因为它正在等待 OpenMP 工作线程唤醒并拾取工作项,但这些线程根本不存在,因为 fork(2) 仅分叉调用线程。执行要么永远不会进入并行区域的主体,要么挂在其末端的隐式屏障上。
        猜你喜欢
        • 2014-04-03
        • 1970-01-01
        • 2013-12-09
        • 2019-06-09
        • 1970-01-01
        • 2015-08-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多