Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make this function thread safe and fast?

int f(int);

Multiple threads can call this function. The function should return

argument * argument_used_in_first_call_to_function

I have coded as below. Even though it is thread-safe it is not fast as it uses mutex lock/unlock. Is there a faster solution while still being thread safe?

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;
}
like image 474
Medicine Avatar asked Sep 04 '15 04:09

Medicine


2 Answers

IF you have a c++11 compliant compiler
(e.g. NOT VS2013)

Both, the easiest and most efficient way is to just write:

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

The c++11 standard requires that initialization of function local static variables is thread safe *). To be more precise, it requires that only one thread initializes the variable and that all other threads wait, until the initialization is completed (later reads and writes can of course still race, but as this is the only write access to firstArg, no additional synchronization is required here).

If your compiler doesn't support "magic statics"

The next best method is to use std::call_once as suggested by Sebastian Redl, which has the same semantics.

If initialization via std::call_once is too slow (it probably uses a mutex) and arg is a built-in type (like int), you can try the following (I didn't do any measurements):

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 is some value that can not possibly be passed to f as a valid argument. In this case std::numeric_limits<int>::max() would cause integer overflow when multiplied with itself, so it is not a valid argument for f and can thus serve as an indicator that firstArg hast not been initialized yet.

Warning: Only use this if you have verified, that std::call_once is unacceptably slow for your particular workload (which will almost never be the case) and that this version is actually a sufficient improvement.

Note on conditional locking

As there are/were some answers that proposed various faulty conditional locking algorithms, I also put up a correct manual implementation of double checked locking.

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;
}

The two important parts are:

  • Do double checking (once inside and once outside of the protected region)
  • Use an std::atomic for the flag. Otherwise, the order, in which threads that don't lock observe the stores to the flag and the variable, is not guaranteed.

Acknowledgements:
The double checked locking version is based on the presentation by Herb Sutter on cppcon2014 and was augmented based on the comments/answers by EOF and Sebastian.


*) See e.g. this question and from the latest working draft of the c++14 standard (6.7 point 4):

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

like image 191
MikeMB Avatar answered Oct 09 '22 04:10

MikeMB


Mike's magic static answer is the best if your compiler supports them. If you're on Visual Studio 2013, the best way to do it is to use std::call_once, not custom flags and mutexes.

#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;
}
like image 30
Sebastian Redl Avatar answered Oct 09 '22 04:10

Sebastian Redl