使用 std::async 创建的线程进行 MPI 发送的线程安全

Posted

技术标签:

【中文标题】使用 std::async 创建的线程进行 MPI 发送的线程安全【英文标题】:thread safety of MPI send using threads created with std::async 【发布时间】:2013-01-27 23:54:24 【问题描述】:

根据this website,MPI::COMM_WORLD.Send(...) 的使用是线程安全的。然而,在我的应用程序中,我经常(并非总是)遇到死锁或出现分段错误。用mutex.lock()mutex.unlock()MPI::COMM_WORLD 方法的每个调用括起来始终可以消除死锁和段错误。

这就是我创建线程的方式:

const auto communicator = std::make_shared<Communicator>();
std::vector<std::future<size_t>> handles;
for ( size_t i = 0; i < n; ++i )

   handles.push_back(std::async(std::launch::async, foo, communicator));

for ( size_t i = 0; i < n; ++i )

   handles[i].get();

Communicator 是一个类,它有一个std::mutex 成员并专门调用诸如MPI::COMM_WORLD.Send()MPI::COMM_WORLD.Recv() 之类的方法。我不使用任何其他通过 MPI 发送/接收的方法。 foo 接受 const std::shared_ptr&lt;Commmunicator&gt; &amp; 作为参数。

我的问题:MPI 承诺的线程安全是否与std::async 创建的线程不兼容?

【问题讨论】:

【参考方案1】:

MPI 中的线程安全并非开箱即用。首先,您必须确保您的实现实际上支持同时进行 MPI 调用的多个线程。对于某些 MPI 实现,例如 Open MPI,这需要在构建时使用特殊选项配置库。然后你必须告诉 MPI 在适当的线程支持级别进行初始化。目前 MPI 标准定义了四个级别的线程支持:

MPI_THREAD_SINGLE - 表示用户代码是单线程的。如果使用MPI_Init(),这是初始化 MPI 的默认级别; MPI_THREAD_FUNNELED - 表示用户代码是多线程的,但只有主线程进行 MPI 调用。主线程是初始化 MPI 库的线程; MPI_THREAD_SERIALIZED - 表示用户代码是多线程的,但对 MPI 库的调用是序列化的; MPI_THREAD_MULTIPLE - 表示用户代码是多线程的,所有线程都可以随时进行 MPI 调用而无需任何同步。

为了使用线程支持初始化 MPI,必须使用 MPI_Init_thread() 而不是 MPI_Init()

int provided;

MPI_Init_thread(&argc, &argv, MPI_THREAD_MULTIPLE, &provided);
if (provided < MPI_THREAD_MULTIPLE)

    printf("ERROR: The MPI library does not have full thread support\n");
    MPI_Abort(MPI_COMM_WORLD, 1);

具有废弃(并从 MPI-3 中删除)C++ 绑定的等效代码:

int provided = MPI::Init_thread(argc, argv, MPI::THREAD_MULTIPLE);
if (provided < MPI::THREAD_MULTIPLE)

    printf("ERROR: The MPI library does not have full thread support\n");
    MPI::COMM_WORLD.Abort(1);

线程支持级别的顺序如下:MPI_THREAD_SINGLE MPI_THREAD_FUNNELED MPI_THREAD_SERIALIZED MPI_THREAD_MULTIPLE,因此任何其他提供的级别,与MPI_THREAD_MULTIPLE 不同,数值都会更低——这就是if (...) 的原因上面的代码是这样写的。

MPI_Init(&amp;argc, &amp;argv) 等价于MPI_Init_thread(&amp;argc, &amp;argv, MPI_THREAD_SINGLE, &amp;provided)。实现不需要精确地在请求的级别进行初始化,而是可以在任何其他级别(更高或更低)进行初始化,这在provided 输出参数中返回。

有关更多信息 - 请参阅 MPI 标准的第 12.4 节,免费提供 here。

对于大多数 MPI 实现,MPI_THREAD_SINGLE 级别的线程支持实际上等同于MPI_THREAD_SERIALIZED 级别提供的支持 - 正是您在案例中观察到的。

由于您没有指定使用哪种 MPI 实现,这里有一个方便的列表。

我已经说过,Open MPI 必须在编译时启用正确的标志才能支持MPI_THREAD_MULTIPLE。但是还有另一个问题 - 它的 InfiniBand 组件不是线程安全的,因此 Open MPI 在全线程支持级别初始化时不会使用本机 InfiniBand 通信。

英特尔 MPI 有两种不同的风格 - 一种支持全多线程,另一种不支持全多线程。通过将 -mt_mpi 选项传递给 MPI 编译器包装器来启用多线程支持,从而启用与 MT 版本的链接。如果启用了 OpenMP 支持或自动并行器,则也隐含此选项。我不知道启用全线程支持时 IMPI 中的 InfiniBand 驱动程序如何工作。

MPICH(2) 不支持 InfiniBand,因此它是线程安全的,并且可能最近的版本提供了开箱即用的 MPI_THREAD_MULTIPLE 支持。

MVAPICH 是构建英特尔 MPI 的基础,它支持 InfiniBand。我不知道它在具有 InfiniBand 的机器上使用时在全线程支持级别上的表现如何。

关于多线程 InfiniBand 支持的说明很重要,因为现在许多计算集群都使用 InfiniBand 结构。禁用 IB 组件(Open MPI 中的openib BTL)后,大多数 MPI 实现会切换到另一种协议,例如 TCP/IP(Open MPI 中的tcp BTL),这会导致通信速度更慢且延迟更多。

【讨论】:

非常有帮助的答案,非常感谢!由于我必须使用多种不同的 mpi 实现,遗憾的是我只能选择使用自己的互斥锁,但这仍然是一个不错的解决方案。 您始终可以选择尝试以完全线程支持来初始化 MPI,并且仅当您没有获得请求的线程支持级别时才使用互斥锁。请注意,能够在 MPI_THREAD_SINGLEMPI_THREAD_FUNNELED 进行序列化的多线程 MPI 调用不是标准指定的行为。 是的,这确实是一种可能。但是从设计的角度来看,我不会那样做。它要么涉及整个通信代码中的条件,要么涉及通信器对象的条件创建。我不喜欢这样,所以我决定完全依赖我自己的互斥锁。恕我直言,它只是更干净一点。 Here 是一篇论文,其中隐含地列出了对 MPI_THREAD_MULTIPLE 的各种其他实现的支持:MVAPICH 1.9a2、IBM MPI 1.2 和 Cray MPI。他们还注意到有关 OpenMPI 和 InfiniBand 的相同问题。他们报告说 IntelMPI 4.0.3 在某些情况下确实支持 Infiniband 和MPI_THREAD_MULTIPLE,但会遇到死锁。请参阅第 5.1 节,从等式开始。 (2) 并记住第 3 节的要求,即底层 MPI 库支持MPI_THREAD_MULTIPLE @alfC,是的,例如两个MPI_Recv 使用相同的源、标签和通信器发布的调用会干扰。集体通信就是这个问题的一个例子,因为包括 Open MPI 在内的许多实现对给定类型的所有集体通信都使用相同的内部标签。这也是 MPI 标准规定每个参与的 rank 必须以完全相同的顺序发出集体操作的原因,当涉及多个线程时,这可能会成为问题。【参考方案2】:

有四个级别的 MPI 线程安全,并非所有实现都支持:MPI_THREAD_SINGLE、MPI_THREAD_FUNNELED、MPI_THREAD_SERIALIZED 和 MPI_THREAD_MULTIPLE。最后一个,它允许一个进程有多个线程可以同时调用 MPI 函数,这可能是你感兴趣的。所以,首先,你需要确保你的实现支持 MPI_THREAD_SERIALIZED。

必须通过调用MPI_Init_thread 来指定所需的线程安全级别。调用 MPI_Init_thread 后,您应该能够在自己创建的 boost (POSIX) 线程中安全地调用 MPI 函数。

【讨论】:

"对 MPI_Init_thread 的调用初始化线程环境并实际创建线程。" -- 该陈述的第二部分是错误的。

以上是关于使用 std::async 创建的线程进行 MPI 发送的线程安全的主要内容,如果未能解决你的问题,请参考以下文章

C++11 多线程std:: async与std::thread的区别

用C++11的std::async代替线程的创建

主线程等待 std::async 完成[重复]

指定线程的 std::async 模拟

std::async()

使用多线程加速(std::async、std::thread 还是?)