【问题标题】:How to make this function thread safe and fast?如何使这个函数线程安全快速?
【发布时间】:2015-11-30 03:25:33
【问题描述】:
int f(int);

多个线程可以调用这个函数。 该函数应该返回

argument * argument_used_in_first_call_to_function

我的编码如下。尽管它是线程安全的,但它并不快,因为它使用互斥锁/解锁。是否有更快的解决方案同时仍然是线程安全的?

mutex mut1;
int f(int x)
{
  pthread_mutex_lock(mut1);
  static bool first_init = true;
  static int first_arg = 0;

  if (first_init)
  {
    first_arg = x;
    first_init = false;
  }
  pthread_mutex_unlock(mut1);
  return x * first_arg;
}

【问题讨论】:

  • 查看 pthread_once。另请参阅 C11/C++11 atomics 以获得更快的标志保护。
  • 请缩进您的代码。
  • C 还是 C++?它们有不同的实现。
  • @black C 或 C++ 都可以
  • 是否有任何支持 POSIX 线程但静态初始化不是线程安全的现代平台?

标签: c++ c multithreading performance static


【解决方案1】:

如果您有兼容 c++11 的编译器
(例如,不是 VS2013)

两者,最简单和最有效的方法就是写:

int f(int x) {
    static int firstArg = x;
    return firstArg*x;
}

c++11 标准要求函数局部静态变量的初始化是线程安全的 *)。更准确地说,它要求只有一个线程初始化变量,并且所有其他线程都等待,直到初始化完成(稍后的读写当然仍然可以竞争,但因为这是对firstArg的唯一写访问,这里不需要额外的同步)。

如果您的编译器不支持“magic statics”

下一个最佳方法是使用 Sebastian Redl 建议的 std::call_once,它具有相同的语义。

如果通过std::call_once 初始化太慢(它可能使用互斥体)并且arg 是内置类型(如int),您可以尝试以下方法(我没有做任何测量):

namespace {
    const int DISALLOWED_VALUE = std::numeric_limits<int>::max();
    std::atomic<int> firstArg= DISALLOWED_VALUE;
}

int f(int x) {
    if (firstArg.load(std::memory_order_relaxed) == DISALLOWED_VALUE) {
        int tmp = DISALLOWED_VALUE;
        firstArg.compare_exchange_strong(tmp, x);
    }   
    return firstArg.load(std::memory_order_relaxed)*x;
}   

DISALLOWED_VALUE 是一些不可能作为有效参数传递给 f 的值。在这种情况下,std::numeric_limits&lt;int&gt;::max() 与自身相乘时会导致整数溢出,因此它不是f 的有效参数,因此可以作为firstArg 尚未初始化的指标。

警告: 仅在您已验证 std::call_once 对于您的特定工作负载(几乎永远不会出现这种情况)的速度慢得无法接受并且此版本实际上已经足够改进时才使用此版本。

条件锁定注意事项

由于有一些答案提出了各种错误的条件锁定算法,我还提出了双重检查锁定的正确手动实现。

namespace { 
    std::atomic<bool> isInit = false; //has to be atomic
    std::mutex mux;
}
int f(int x) {
    static int firstArg;
    if (!isInit.load(std::memory_order_acquire)) {
        std::lock_guard<std::mutex> lg(mux);
        if (!isInit.load(std::memory_order_acquire)) {
            firstArg = x;
            isInit.store(true,std::memory_order_release);
        }
    }
    return firstArg*x;
}

两个重要的部分是:

  • 进行双重检查(一次在受保护区域内,一次在受保护区域外)
  • 使用std::atomic 作为标志。否则,不能保证未锁定的线程观察到标志和变量的存储顺序。

致谢:
双重检查锁定版本基于 Herb Sutter 在 cppcon2014 上的演示,并根据 EOF 和 Sebastian 的 cmets/answers 进行了扩充。


*) 参见例如this question 以及来自 c++14 标准的最新工作草案(6.7 第 4 点):

如果在初始化变量时控制同时进入声明,则并发执行将等待初始化完成。

【讨论】:

  • 错误:static 数据是进程范围的,因此对所有线程都是通用的。
  • @BasileStarynkevitch:这就是 OP 想要的,不是吗?所有线程的值相同。重要的是初始化在 c++11 中是线程安全的:stackoverflow.com/questions/8102125/…。所以不存在数据竞争,即使上面的函数被多个线程同时访问。
  • @MikeMB 我相信你是对的(虽然你应该返回firstArg * x)。
  • @BasileStarynkevitch:我添加了一些额外的解释,希望我的观点现在很清楚。如果您仍然认为我犯了错误 - 或误解了问题 - 请提供一些额外的解释。
  • @BasileStarynkevitch 我知道这是 OP 想要的。如果没有,static thread_local int firstArg = x; 可以解决问题。
【解决方案2】:

如果您的编译器支持,Mike 的神奇静态答案是最好的。如果您使用的是 Visual Studio 2013,最好的方法是使用 std::call_once,而不是自定义标志和互斥锁。

#include <mutex>
namespace {
  std::once_flag fFirstCallFlag;
}
int f(int arg) {
  static int firstValue;
  std::call_once(fFirstCallFlag, [&firstValue, arg] { firstValue = arg; });
  return firstValue * arg;
}

【讨论】:

  • 你确定firstValue 可以是本地静态的吗?我想,如果你不显式初始化它们,它们会在第一次被隐式初始化为零,控制流通过它们。
  • 不,隐式零初始化是静态的,而不是动态的,所以它发生在程序加载时。如果它是一个带有构造函数的对象,或者初始值需要动态计算,那么你是对的。
  • 对,我忘记了这一点(就像我对 call_once 所做的那样)。感谢您的信息。
  • 你知道std::atomic&lt;bool&gt;是什么情况吗?
  • 理论上是安全的(需要有一个 constexpr 值构造函数,这意味着初始化应该是静态的),但我不确定 Visual Studio 2013,它不支持 constexpr。
【解决方案3】:

如果您仍然考虑使用一些锁来实现,请尝试使用自旋锁。它通过自旋将线程保持在用户空间,如果操作很快,则不会切换到内核空间

#include "boost/smart_ptr/detail/spinlock.hpp"

boost::detail::spinlock lock;

bool first_init = true;
int first_arg = 0;

int f(int x)
{
  std::lock_guard<boost::detail::spinlock> guard(lock);
  if (first_init)
  {
    first_arg = x;
    first_init = false;
  }
  return x * first_arg;
}

【讨论】:

    猜你喜欢
    • 2019-03-29
    • 1970-01-01
    • 2012-01-07
    • 1970-01-01
    • 2011-07-18
    • 1970-01-01
    • 2021-06-06
    • 2011-04-06
    相关资源
    最近更新 更多