如何使这个功能线程安全快速?

Med*_*ine 9 c c++ performance static multithreading

int f(int);
Run Code Online (Sandbox Code Playgroud)

多个线程可以调用此函数.该函数应该返回

argument * argument_used_in_first_call_to_function
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

Mik*_*eMB 12

如果你有一个符合c ++ 11标准的编译器
(例如NOT VS2013)

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

int f(int x) {
    static int firstArg = x;
    return firstArg*x;
}
Run Code Online (Sandbox Code Playgroud)

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

如果您的编译器不支持"魔法静态"

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

如果初始化via 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;
}   
Run Code Online (Sandbox Code Playgroud)

DISALLOWED_VALUE是一些不能作为有效参数传递给f的值.在这种情况下std::numeric_limits<int>::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;
}
Run Code Online (Sandbox Code Playgroud)

两个重要部分是:

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

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


*)参见例如这个问题以及c ++ 14标准的最新工作草案(6.7第4点):

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

  • @BasileStarynkevitch:这就是OP想要的,不是吗?alll线程的值相同.重要的是,在c ++ 11中初始化是线程安全的:http://stackoverflow.com/questions/8102125/is-local-static-variable-initialization-thread-safe-in-c11.因此,即使多个thrad同时访问上述函数,也没有数据竞争. (4认同)

Seb*_*edl 6

如果您的编译器支持,那么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;
}
Run Code Online (Sandbox Code Playgroud)