Thread-safe, mutex lock for smart pointer shared ptr

[Smart pointers] and thread safety issues

<1> The smart pointer shared_ptr itself (the underlying implementation principle is reference counting) is [thread-] is [thread-] safe

The reference counting of smart pointers uses atomic atomic operations in the method , as long as the shared_ptr increases the reference when copying or assigning, and reduces the reference when destructing. First of all, atomic is thread-safe, and all smart pointers are reference-counted safely under multi-threading. That is to say, when smart pointers are passed and used under multi-threading, reference counting will not have thread-safety problems.

<2> The thread safety of the object pointed to by the smart pointer, the smart pointer does not make any guarantees

  • problems encountered

The reference count for the smart pointer shared_ptr itself is safe and lock-free, but the read and write of the object is not , because the shared_ptr has two data members, one is the pointer to the object, and the other is the reference count we saw above. For managing objects, when a smart pointer is copied, the implementation of the standard library is to copy the smart pointer first, and then copy the reference counted object (when copying the reference counted object, use_count will be incremented by one). These two operations are not atomic operations. The danger is here. The reference counts of smart pointers in two threads are ++ or — at the same time. This operation is not atomic. Suppose the reference count is 1. After ++ twice, it may still be 2. In this way, the reference count is confused and violates atomicity. .

  • The following three core concepts in multi-threaded programming can be used as explanations for reason analysis in interviews

(1) An example of atomicity

This is similar to the atomic concept of database transactions, that is, an operation (which may contain multiple sub-operations) is either all executed (effective) or none of them are executed (not effective).

A very classic example of atomicity is the bank transfer problem: for example, A and B transfer 100,000 yuan to C at the same time. If the transfer operation is not atomic, when A transfers money to C, it reads the balance of C as 200,000, and then adds 100,000 of the transfer, and calculates that there should be 300,000 at this time, but it is still in the future and writes 300,000 Back to C’s account, when B’s transfer request came, B found that C’s balance was 200,000, then added 100,000 to it and wrote it back. Then A’s transfer operation continues – 300,000 is written back to C’s balance. The final balance of C in this case is 300,000 instead of the expected 400,000.

(2) Examples of visibility

Visibility means that when multiple threads access a shared variable concurrently, the modification of a shared variable by one thread can be immediately seen by other threads. The visibility issue is something that many people overlook or misunderstand.

The efficiency of the CPU reading data from the main memory is relatively inefficient. In mainstream computers, there are several levels of cache. When each thread reads a shared variable, it loads the variable into its corresponding CPU cache, and after modifying the variable, the CPU updates the cache immediately, but does not necessarily immediately write it back to main memory (actually Unpredictable time to write back to main memory). When other threads (especially threads not executing on the same CPU) access the variable, the old data is read from main memory, not the updated data of the first thread.

This is a mechanism at the operating system or hardware level, so many application developers often ignore it.

(3) Sequential example

Sequential means that the order in which the program is executed is executed in the order in which the code is executed. In order to improve the overall execution efficiency of the program, the processor may optimize the code. One of the optimization methods is to adjust the code order to execute the code in a more efficient order. Speaking of this, some people are in a hurry – what, the CPU does not execute the code in the order of my code, so how can we guarantee the effect we want? In fact, you can rest assured that although the CPU does not guarantee that it will be executed completely in the code order, it will guarantee that the final execution result of the program is the same as the result of the code order execution.

  • Solution – join mutex

Use mutex to lock multiple threads reading and writing the same shared_ptr (when multiple threads access the same resource, in order to ensure data consistency, the easiest way is to use mutex (mutual exclusion lock))

Once a thread acquires the lock object, it is always protected in the critical section, which means that the thread has been occupying resources.

Description of critical sections

Sometimes we will encounter a situation where two processes/threads share the same resource, which is called a critical section. A critical section is a section of code that can only be executed by one thread at a time

  • Code display for adding mutex
    • Method 1: Directly operate the mutex, that is, directly call the lock / unlock function of the mutex

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  mutex.lock();

  int i = ++count;
  std::cout << "count == " << i << std::endl;

  // If there is an exception in the previous code, the unlock will not be adjusted.
  mutex.unlock();
}

int  main ()  {
   // Create a set of threads.
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  // Wait for all threads to finish.
  threads.join_all();
  return 0;
}

  • Method 2: Use lock_guard to automatically lock and unlock. The principle is RAII, similar to smart pointers

C++ takes advantage of a very nice feature: the constructor is automatically called when an object is initialized, and the destructor is automatically called when an object reaches the end of its scope. So we can use this feature to solve the lock maintenance problem: encapsulate the lock inside the object! At this point, the lock is acquired during the constructor, and the destructor is automatically called to release the lock before the statement returns. In fact, this practice has a proper name, called RAII

#include <iostream>
#include <boost/thread/lock_guard.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void  Counter ()  {
   // lock_guard locks in the constructor and unlocks in the destructor.
  boost::lock_guard<boost::mutex> lock(mutex);

  int i = ++count;
  std::cout << "count == " << i << std::endl;
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

Leave a Comment

Your email address will not be published. Required fields are marked *