【问题标题】:Difference between std::lock_guard and #pragma omp criticalstd::lock_guard 和 #pragma omp critical 之间的区别
【发布时间】:2021-05-12 19:52:28
【问题描述】:

让我们考虑一些代码来安全地在具有多个线程的 for 循环中递增变量。

要实现这一点,您必须在增加变量时使用某种锁定机制。 当我在寻找解决方案时,我想出了以下解决方案。

我的问题是:

  1. 它们是否同样好,或者其中一个有一些后备?
  2. 何时使用mutex 而不是#pragma omp critical
#include <iostream>
#include <mutex>

int main(int argc, char** argv)
{
    int someVar = 0;
    std::mutex someVar_mutex;

    #pragma omp parallel for
    for (int i = 0; i < 1000; i++)
    {
        std::lock_guard<std::mutex> lock(someVar_mutex);
        ++someVar;
    }

    std::cout << someVar << std::endl;

    return 0;
}
#include <iostream>

int main(int argc, char** argv)
{
    int someVar = 0;

    #pragma omp parallel for
    for (int i = 0; i < 1000; i++)
    {
        #pragma omp critical
        ++someVar;
    }

    std::cout << someVar << std::endl;

    return 0;
}

【问题讨论】:

  • 原则上,如果一个的想法,混合两个不同的并行模型。因此,如果您使用 OpenMP 并行性,请避免使用 C++ 之一,因为两者之间的交互可能会出乎意料。现在,就是说,你想达到什么目的?因为根据您想对 someVar 执行的操作,可能会有更好的方法来增加它。
  • 从现有的开源 C++ 项目中汲取灵感,例如 FLTKfishRefPerSys。对于 RefPerSys,请通过电子邮件与我联系至basile@starynkevitch.net

标签: c++ multithreading performance openmp mutex


【解决方案1】:

临界区与获取锁的作用相同(并且可能会在内部使用锁)。

  1. std::mutex 是标准 C++ 功能,而 #pragma omp critical 是 OpenMP 扩展,未由标准定义。

  2. 临界区名称对整个程序都是全局的(与模块边界无关)。因此,如果您在多个模块中有同名的临界区,则不能同时执行其中两个。如果省略名称,则假定为默认名称。 (docs)。

更喜欢标准 C++,除非有充分的理由使用另一个(在衡量两者之后)。

不是直接针对问题,但是这个循环还有一个问题:每次循环迭代都会执行锁。这会显着降低性能(另请参阅 this answer)。

【讨论】:

  • 需要注意的一点:“#pragma omp critical”只能与其他“critical”结构交互。您不能将 C++ 锁和 OpenMP 锁(锁 API 或“关键”构造)与 C++ 锁(如 std::mutex)混合使用。因此,您有使用 std::mutex (或顶部的 std::lock_guard )保护的代码,然后其他应该互斥的 OpenMP 代码也需要使用 std::mutex (反之亦然)。关于性能:C++ 和 OpenMP 都在一个锁实现动物园上,所以如果你选择类似的锁实现,你通常会看到非常相似的性能。
【解决方案2】:

来自cppreference.comlock_guard 可以阅读

lock_guard 类是一个互斥体包装器,它提供了一个方便的 RAII 风格的机制,用于在作用域期间拥有互斥锁 块。

OpenMP 标准关于critical 可以阅读:

关键构造限制了相关联的执行 结构化块一次到一个线程。

因此,两种机制都提供了处理相同问题的方法确保代码块的mutual exclusion

它们是否同样好,或者其中一个有一些后备?

两者都是粗粒度锁定机制,但是,默认情况下,OpenMP critical 的粒度更粗,因为:

所有没有名称的关键结构都被认为具有相同的未指定名称。

因此,如果未指定名称,则所有关键区域都使用相同的全局锁,这在语义上与使用lock_guard 和相同的mutex 相同。尽管如此,可以与critical pragma 一起指定一个名称:

可选名称可用于标识关键构造。

#pragma omp critical(name)

critical 上指定name 在语义上类似于将锁传递给std::lock_guard&lt;std::mutex&gt; lock(name);

OpenMP 还提供了明确的锁定机制,例如 omp_lock_tSO Thread 中的一些详细信息),这一点毫无价值。

尽管如此,您应该尽可能地瞄准比关键区域更精细的同步机制,即reductionatomics,甚至使用数据冗余。例如,在您的代码 sn-p 中,最高效的方法是使用 reduction 子句,如下所示:

#pragma omp parallel for(+:someVar)
for (int i = 0; i < 1000; i++)
{
    ++someVar;
}
  1. 何时使用互斥锁而不是 #pragma omp critical?

IMO 这不应该是一个考虑因素,首先因为正如Michael Klemm 所指出的那样:

需要注意的一点:“#pragma omp critical”只能 与其他“关键”构造交互。您不能混合使用 C++ 锁 和带有 C++ 锁的 OpenMP 锁(锁 API 或“关键”构造) 像 std::mutex 一样。所以,你有使用保护的代码 std::mutex (或顶部的 std::lock_guard ),然后是其他 OpenMP 代码 应该是互斥的,还需要使用 std::mutex (反之亦然 反之亦然)。

此外,正如Gilles 指出的那样(我也有同样的看法):

原则上,混合两种不同的并行模型是一种 馊主意。因此,如果您使用 OpenMP 并行性,请避免将 C++ 用作 两者之间的互动可能是出乎意料的。

【讨论】:

    猜你喜欢
    • 2021-03-22
    • 1970-01-01
    • 2021-06-08
    • 1970-01-01
    • 2021-09-07
    • 1970-01-01
    • 1970-01-01
    • 2012-05-06
    相关资源
    最近更新 更多