C++ Core Guidelines: The Remaining Rules to Lock-Free Programming

Contents[Show]

Today, I will finish my story to concurrency and lock-free programming in particular. There are four rules to lock-free programming in the C++ core guidelines left.

 padlock

First of all, here are the rules for the current post.

I have to admit, I annoyed a few German readers with my two last posts about lock-free programming. My readers got the impression, that I don't like lock-free programming. Wrong!. I'm totally curious about lock-free programming but before you use it you have to answer two questions.

  1. Does lock-free programming solve my performance bottleneck?
  2. Do I understand lock-free programming good enough to use it?

Before you can not answer this two questions with a big yes, you should continue with the rule CP.102

CP.101: Distrust your hardware/compiler combination

What does that mean: distrust your hardware/compiler combination. Let me put in in another way: When you break the sequential consistency, you will also break with high probability your intuition. Here is my example:

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> x{0};
std::atomic<int> y{0};

void writing(){  
  x.store(2000);                        // (1)
  y.store(11);                          // (2)
}

void reading(){  
  std::cout << y.load() << " ";         // (3)
  std::cout << x.load() << std::endl;   // (4)
}

int main(){
  std::thread thread1(writing);
  std::thread thread2(reading);
  thread1.join();
  thread2.join();
}

 

I have a question for the short example? Which values für y and x are possible in the lines (3) and (4). x and y are atomic, therefore no data race is possible. I further don't specify the memory ordering, therefore, sequential consistency applies. Sequential consistency means:

    • Each thread performs its operation in the specified sequence: line (1) happens before the line (2) and line (3) happens before the line(4).
    • There is a global order of all operations on all threads.

If you combine this two properties of the sequential consistency, there is only one combination of x and y not possible: y == 11 and x == 0.

Now, let me break the sequential consistency and maybe your intuition. Here is the weakest of all memory orderings: the relaxed semantics.

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> x{0};
std::atomic<int> y{0};

void writing(){  
  x.store(2000, std::memory_order_relaxed);   // (1)
  y.store(11, std::memory_order_relaxed);     // (2)
}

void reading(){  
  std::cout << y.load(std::memory_order_relaxed) << " ";        // (3)
  std::cout << x.load(std::memory_order_relaxed) << std::endl;  // (4) 
}

int main(){
  std::thread thread1(writing);
  std::thread thread2(reading);
  thread1.join();
  thread2.join();
}

 

Two unintuitive phenomena can happen. First, thread2 can see the operations of thread1 in a different sequence. Second, thread1 can reorder its instruction because they are not performed on the same atomic.  What does that mean for the possible values of x and y: y == 11 and x == 0 is a valid result. I want to be a little bit more specific. Which result is possible depends on your hardware.

For example, operation recording is quite conservative on x86 or AMD64, stores can be reordered after loads but on Alpha, IA64, or RISC (ARM) architectures, all four possible reordering of stores and loads operations are allowed.

 LoadStore

If you don't believe me, I suggest you read the following rule CP.102.

CP.102: Carefully study the literature

There is not much to add to this rule. At least, I can provide links to the literature.

CP.110: Do not write your own double-checked locking for initialization and CP.111: Use a conventional pattern if you really need double-checked locking

I know, I should not write about the singleton pattern but the double-checked locking pattern is infamous for initialising a singleton in a thread-safe way. Here we are:

std::mutex myMutex;

class MySingleton{
public:  
  static MySingleton& getInstance(){    
    std::lock_guard<std::mutex> myLock(myMutex);       // (1)  
    if( !instance ) instance= new MySingleton();    
    return *instance;  
  }
private:  
  MySingleton();  
  ~MySingleton();  
  MySingleton(const MySingleton&)= delete;  
  MySingleton& operator=(const MySingleton&)= delete;
  static MySingleton* instance;
};
MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;
MySingleton* MySingleton::instance= nullptr;

 

This implementation of the singleton pattern is thread-safe because each access to the instance is protected by a std::lock_guard (line (1)). The implementation is correct but way to expensive because each reading access of the singleton is guarded by a heavy-weight lock. Beside the initialisation of the singleton, no synchronisation is necessary. Here comes the double-checked locking pattern to our rescue.

static MySingleton& getInstance(){    
  if ( !instance ){                              // (1)               
    lock_guard<mutex> myLock(myMutex);           // (2)   
    if( !instance ) instance= new MySingleton(); // (3)
  }  
  return *instance; 
}

The getInstance method uses now an inexpensive pointer comparison in line (1) instead of an expensive lock. Only if the pointer is a nullptr, an expensive lock is used (line (2)). Because there is the possibility that another thread will initialise the singleton between the pointer comparison (line (1)) and the lock (line (2)), an additional pointer comparison in line (3) is necessary. So the name is obvious. Two times a check and one time a lock.

Smart? Yes! Thread safe? No!

What is the problem? The call instance= new MySingleton() in line (3) consists of at least three steps.

  1. Allocate memory for MySingleton
  2. Create the MySingleton object in the memory
  3. Let instance refer to the MySingleton object

The problem is that there is no guarantee about the sequence of these three steps. For example, the processor can reorder the steps to the sequence 1,3 and 2. So, in the first step the memory will be allocated and in the second step, the instance refers to the singleton. If at that time another thread tries to access the singleton, it compares the pointer and assumes that the singleton is fully initialised.

The consequence is simple: the program has undefined behaviour.

I have already written a quite emotionally discussed post to the thread-safe singleton pattern. This included different implementations with std::lock_guard, std::call_once and std::once_flag, the Meyers singleton, and atomic versions that are based on the double-checked locking-pattern. You can read the details to these implementations and their different performance characteristics on Linux and Windows here:  Thread-Safe Initialization of a Singleton.

What's next?

As I promised I'm done with the rules to concurrency. The next post is about the rules for error handling in the C++ core guidelines.

 

 

 

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter,  Sai Raghavendra Prasad Poosa, Meeting C++, Matt Braun, Avi Lachmish, Adrian Muntea, and Roman Postanciuc.

 

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 976

All 950079

Currently are 191 guests and no members online

Kubik-Rubik Joomla! Extensions