robby987
robby987

Reputation: 827

Fast lock for variables that are read a lot and may be changed from another thread occasionally

I'm looking for a lock, that allows a thread-safe transition between the GUI and the back-end.

Just for a double, but I'm sure it will end up being used for other things.

Now this is the part I'm unsure of, on modern CPUs, can reading and writing at the same time cause a race condition? Or is it only when two threads try to write at the same time.

I've always encapsulated any variables that I cross threads with, in a template object, which allows me the same use but requires locking, here is the basics:

//=====================================================================================================
// Class to store a variable in a thread safe manner.
//=====================================================================================================
template <class T>
class ThreadSafeVariable
{
public:

  ThreadSafeVariable(const T & variable):
    _stored_variable(variable)
  {
  }

  ThreadSafeVariable():
    _stored_variable()
  {
  }

  //=====================================================================================================
  /// Returns the stored variable
  //=====================================================================================================
  T Read() const
  {
    boost::unique_lock<boost::mutex> lock(_mutex);
    return _stored_variable;
  }


  //=====================================================================================================
  /// Sets the variable
  //=====================================================================================================
  void operator = (const T &value_to_set)
  {
    boost::unique_lock<boost::mutex> lock(_mutex);
    _stored_variable = value_to_set;
  }


  //=====================================================================================================
  /// Returns the stored variable
  //=====================================================================================================
  operator T() const
  {
    boost::unique_lock<boost::mutex> lock(_mutex);
    return (T) _stored_variable;
  }


  void SetFromString (const std::string & value_to_set);
  T operator ++ (int);
  T operator -- (int);
  std::string ToString() const;

  protected:
  T _stored_variable;
  mutable boost::mutex _mutex;
};

Would it be possible, to make a class like this faster, provided that only one thread was given the option of writing (that part would needed to be encoded by calling different functions).

Basically I've got a static function, that I want to keep static that varies based on a parameter I would like to change on the GUI but it's in a performance critical part of the software.

I'm aware of spin locks, atomics. But have never really used them. I guess a spin lock would be a waste of CPU and I'm not sure of the performance gains from atomics.

Upvotes: 2

Views: 1490

Answers (1)

You should take a look at std::atomic<>, it pretty much implements precisely the behavior you need. Don't reinvent the wheel.

The nice thing about std::atomic<> is, that it actually employs the hardware facilities for atomic reads and writes, so that all the basic instanciations of std::atomic<> are actually lockfree, which is a huge speed bonus. There are even a few preprocessor macros that signal which of the basic instanciations of std::atomic<> are implemented in a lockfree fashion, which allows you to select the best fitting lockless one (ATOMIC_CHAR_LOCK_FREE is one of these, for example).


The following is an explanation of the possible race between a read and a concurrent write only. Don't try to read this as instructions for doing anything outside of the language standard. If you ignore the standard and nasal deamons appear, that's not in my department.

Regarding your question about whether there can be a race condition between a read and and write, that really depends on the circumstances. Generally, you can assume that a single read or write of a naturally aligned type that is natively supported by the machine will be handled in an atomic fashion. I. e., on a 64 bit machine you can expect reads and writes of uint64_t to be atomic if the uint64_t is aligned to an eight byte boundary. If these conditions are not met, you may end up in a situation where one half of the value you read from memory comes from the value before the write while the other half comes from the value that was written. For example, if you do this

while(1) {
    myGlobal = 0x0000000000000000;
    myGlobal = 0x0123456789abcdef;
}

concurrent to

printf("0x%016llu\n", myGlobal);

and myGlobal is not properly aligned, or is run on a 32-bit machine, you might find that you get an output of 0x0123456700000000.

The C++ language is defined in a way that ignores these implementation details, so concurrent access of any variable that includes at least one write is considered a race condition. This is a bit far on the safe side, as the existence of std::atomic<> acknowledges. It is also very dangerous to code that relies on these guarantees but does not use std::atomic<>, because it allows the optimizer to skrew the assumptions of the programmer (more information on this can be found in this short article Boehm.pdf , thanks to nosid for the link).

Consequently, std::atomic<> is the only way to get lockless, threadsafe variables with the blessing of the C++ standard.

Upvotes: 2

Related Questions