Multi-threaded thread synchronization

thread synchronization

  • What can go wrong:
    • Unsynchronized data access: Two threads running in parallel read and write the same data, and it is not known which statement comes first
    • Data written halfway: a thread is reading data, and another thread changes it, so the reading thread may read half-changed data, and read a half-new half-old value
    • Rearranged Statements: Statements and operations have the potential to be reordered, and since C++ only requires compiled code to be observablely correct within a single thread, it is possible to rearrange data as long as the single thread has the same visual effect
  • Properties needed to solve the problem:
    • Inseparability: reading and writing a variable or statement whose behavior is exclusive
    • Sequence: Guaranteed to execute in the specified order of statements
    • Solutions (sorted from high to low):
      • Both future and promise guarantee inseparability and order: the shared state must be set after the result is formed
      • mutex and lock: granting exclusive rights
  • condition variable:
  • atomic operation
    • Low-level interface for atomic operations: it allows to relax the order of atomic statements or use finger fences for memory access - to be verified

mutex

mutex and lock: exclusive access to resources
* mutex: can only be locked by one thread at the same time, multiple locks of the same lock will cause deadlock; lock through the member function lock(), and unlock by unlockc
* recursive_mutex: allow the same time The same thread has acquired its lock multiple times, allowing the same thread to lock multiple times, and release the lock at the latest corresponding unlock
* time_mutex: additionally allows passing a time period or point in time, used to define how long it can try to capture A lock; for this it provides try_lock_for() and try_lock_until()
* recursive_timed_mutex: Allows the same thread to acquire lock multiple times, you can specify the deadline
* try_lock(): If you want to get a lock, but you don't want to block if you don't succeed; this function locks successfully Return true, otherwise return false
* In order to be able to use lock_guard, you can pass an additional parameter adopt_lock to its constructor. Note:
+ try_lock may fail falsely, that is, the lock has not been taken away, but it may also fail
+ Do not use protected data pointer or reference to

  • In order to wait for a specific length of time, you can use time mutex: timed_mutex and resursive_timed_mutex, which allow calling try_lock_for and try_lock_until to wait for a certain period of time or reach a certain point in time
  • recursive_mutex: nested lock
  • Trial lock: try to obtain a lock, if you can't succeed, you don't want to be blocked forever, try_lock, in order to be able to use lock_guard, you can pass an additional argument adopt_lock to the constructor
  • abnormal:
  • The second lock throws an exception std::system_error with error code resource_deadlock_would_occur
    • lock: Multiple mutexes can be locked at one time without side effects
    • lock_guard<…>(…):
    • unique_lock<>():

lock_guard和unique_lock

  • Advantages: The mutex is locked when it is destructed, and its destructor will automatically call unlock(). If the mutex is not locked, the destructor will not do anything
  • Compared with lock_guard, unique_lock adds three constructs:
    * try_to_lock: attempt to lock mutex but do not want to be locked unique_lock lock(mutex, std::try_to_lock)
    * pass a time point or period to the construct, try to be in an explicit Lock unique_lock<timed_mutex> lock(mutex, std::chrono::seconds(1))
    * pass defer_lock, indicating that this lock object is initialized but not yet intended to be locked by mutex unique_lock
    * In addition, unique_lock provides release to release mutex, or transfer ownership of the mutex to another lock
  • Unique_lock is used in conjunction with condition variables, first locked with lock, then the wait of the condition variable will check these conditions, and return when the conditions are met, if the conditions are not met, wait will unlock the mutex and put the current thread in a waiting state; When the thread preparing the data calls notify_one/norify_all to notify the condition variable, the thread processing the data wakes up from the sleep state, reacquires the mutex and checks the condition again
  • Reviews:
    • When used alone, unique_lock and lock_guard both lock and release locks, and there is no unique_lock that consumes more resources (just one more bool field). unique_lock can be used with condition variables
    • wait( std::unique_lockstd::mutex& lock ) function source code cannot jump in
  • call_once: The first actual parameter must be the corresponding once_flag to ensure that the function passed in is only executed once, and the next actual parameter is a callable object; compared to locking the mutex and displaying the check pointer, each thread only needs Using his std::call_once, at the end of std::call_once, you can safely know that the pointer has been initialized by other threads. Using std::call_once consumes less resources than using a mutex explicitly

condition variable

#include<condition_variable>,condition_variable和condition_variable_any

  • It works as follows:
    a. Must #include, #include<condition_variable>, and declare a mutex and a condition_variable
    b. The thread whose trigger condition is finally met must call notify_one or notify_all
    c. The waiting thread must call std::unique_lock l(readyMutex) ; readyCondVar.wait(1)
    d. condition_variable: limited to work with std::mutex, multiple threads can wait for a specific condition to occur, once the condition is met, if the condition_variable cannot be established, the constructor will throw a std::system_error exception
  • method:
    • wait(ul)/wait(ul, pred): Use unique lock to wait for notification/until pred returns true after a wake-up. The thread waiting for the condition to be satisfied must call wait; inside wait will explicitly unlock and lock the mutex
    • wait_for(ul, duration) / wait_for(ul, duration, pred): use unique lock ul to wait for the notification, the waiting period is duration/or know that pred is true after a wake-up
    • wait_until(ul, timepoint)/wait_until(ul, timepoint, pred): Use unique lock ul to wait for the notification until the timepoint timepoint/or until the result of pred is true after a wake-up
    • notify_one()/notify_all(): The thread that triggers the condition must call notify_one or notify_all
  • Note: The condition variable may have a false wakeup, that is, the wait action may return when the condition has not been notified, so after waking up, it is necessary to verify whether the condition has been met
  • condition_variable_any: can work with any mutex that meets the minimum criteria, but will incur additional overhead

atomic operation

#include, atomic operation is a kind of indivisible operation, which cannot be decomposed into basic types, including integer type, real type, etc. When such an operation is in the middle, you can't check it. Its status is either completed or not completed.

  • std::atomic_flag: Simple Boolean flag, all operations of this type need to be lock-free. Can toggle between two states: set and cleared
    • Features: Compared with other atomic types, atomic_flag is lock-free; and does not provide the is_lock_free() member function; atomic is not lock-free, and the atomicity of operations is guaranteed in the future. A built-in mutex is required in its implementation
    • The default memory order of each atomic operation is memory_order_seq_cst
    • Defect: This method has strong limitations, there is no non-modifying query operation, and it cannot be used like a bool flag, so it is best to use std::atomic
    • Initialization: When used for the first time, it must be initialized with the value ATOMIC_FLAG_INIT, indicating the clear state, ie false
    • A std::atomic_flag object cannot be copy-constructed, and one object cannot be assigned to another std::atomic_flag object. This is not specific to atomic_flag, but common to all atomic types.
    • When the flag object has been initialized, only three things can be done: destroy, clear, set
    • method:
      • Destroy: clear()
      • Query or set: test_and_set()
      • atomic_flag is very suitable for doing spin locks
  • spin lock:
    • Compared with the mutex, the spin lock does not block the thread when acquiring the lock, but keeps spinning to try to acquire the lock. When the thread is waiting for the spin lock, the CPU cannot do other things, but is always in a busy waiting state.
    • Main applicable scenarios: mainly applicable to situations where the holding time is short and the thread does not want to spend too much time on rescheduling
    • Note when using a spin lock: Since the CPU is not released during spin, if the spin lock is used in a scenario where the lock is held for a long time, the CPU will be consumed in meaningless busy waiting until the time slice of this thread is exhausted. Therefore, the thread holding the spin lock should release the spin lock as soon as possible.
      std::atomic: atomic type can only be constructed from template parameters, copy construction and copy assignment, move construction are not allowed, but from atomic type It is possible to construct a variable whose template parameter T is a variable.
      The atomic object should always be initialized, because the default constructor does not necessarily fully initialize it (it is not that the initial value is ambiguous, but its lock is not initialized). If only the default constructor is used, the only legal next step is to call: (How to initialize static variables?)
      std::atomic readyFlag;
      std::atomic_init(&readyFlag, false);
  • method:
    • a.store(val): Assign a new value val and return void
    • a.load(): returns a copy of the value a
    • a.exchange(val): exchange val and return a copy of the old value a
    • atomic a; atomic_init(&a, val): Initialize a
    • a.is_lock_free(): If the lock is not used internally, it will return true——specific function? Only the std::atomic_flag type does not provide the is_lock_free() member function
    • a.compare_exchange_strong(exp, des): It is not clear what these two functions do
    • a.compare_exchange_weak(exp, des)
    • a.fetch_add(val): Indivisible t+=val? , returns a copy of the new value
    • a.fetch_sub(val)
    • a+=with / a-=with
    • a++/++a/a–/–a
    • a.fetch_and(val)/a.fetch_or(val)/a.fetch_xor(val)
    • a&=with/a|=with/a^=with

template std::atomic<>

It exists to allow the user to create an atomic variable with a custom type in addition to the standard atomic types.

  • Requirements for custom types to use this template: Whether the custom type is required to be a POD type
  • There must be a copy assignment operator, which means that this type cannot have any virtual functions or virtual base classes, and must use the copy assignment operator created by the compiler—?
  • This type must be bit-comparable, can be copied using memcpy, and its object must be bit-comparable using memcmp, the reason for this requirement is to ensure that the comparison and exchange operations can work normally
  • Normally the compiler will not generate lock-free code for the std::atomic<> type, so it will use an internal lock for all operations
  • A C-style interface to atomic: an extension to the C standard
    • atomic_init
    • atomic_store
    • atomic_load
  • atomic low-level interface: means that sequential consistency is not guaranteed when using atomic operations,

memory model

Memory order of atomic operations: default memory_order_seq_cst, representing three memory models:

  • Sort consensus sequences:
  • memory_order_seq_cst
  • Acquisition-release sequence: atomic load (memory_order_acquire) is the acquisition operation, atomic storage (memory_order_release) is the release operation, here either acquisition or release, or both operations (memory_order_acq_rel)
memory_order_consume
memory_order_acquire:原子加载
memory_order_release:原子存储
memory_order_acq_rel
  • Free sequence: I don't know much about
    memory_order_relaxed

Design of Concurrent Data Structures

Need to think about how to minimize serialized access and maximize real concurrency:

  1. Are operations within the scope of the lock allowed to be performed outside the lock?
  2. Can different regions of a data structure be protected by different mutexes?
  3. Do all operations need same-level mutex protection?
    Lock-based concurrent data structure design needs to ensure that the access thread holds the lock for the shortest
    possible time. Possible problems:
  4. Inadvertently passing a reference to protected data: Do not pass pointers or references to protected data outside the mutex, such as passing outside through return values ​​​​or parameters - C++ Concurrent Programming 57
  5. Conditional competition that occurs on the interface: C++ concurrent programming 60
    One of the most important new features in C++11 is the multithread-aware memory model
    Memory model: usually a hardware concept, which means that machine instructions are based on What kind of order is executed by the processor
    • Strong order: executed in order, X86 is a strong order model
    • Weak order: executed out of order, which can make instruction execution more performant
    Sequential consistency:
    • In C++11 Member functions of atomic types always guarantee sequential consistency. For the x86 platform, the compiler's rearrangement optimization between atomic type variables is prohibited, and memory_order_seq_cst is the default. The solution given by the designer in C++11 is to let the programmer specify the so-called memory order for atomic operations: memory_order
    • memory_order_relaxed: free sequence, without any synchronization relationship
    • memory_order_acquired: in this thread, all subsequent operations
    • memory_order_release:
    • memory_order_acq_rel:
    • memory_order_consume:
    • memory_order_seq_cst: all executed sequentially: sequence consistent is the simplest and most intuitive sequence, but also the most expensive memory sequence, since global synchronization for all threads is required

lock-free data structures

volatile: used to provide over-optimization, neither guarantees indivisibility, nor guarantees the order. The
terminate function calls the abort function by default, but the user can change the default behavior through the set_terminate function.
The abort function is more low-level, and abort will not be called. Any destructor
exit is a normal exit, the destructor of the automatic variable will be called, and the function registered by atexit will also be called, which is the same as the cleanup work at the end of the main function. The C++11 standard introduces quick exit: quick_exit,
at_quick_exit

Guess you like

Origin blog.csdn.net/u010378559/article/details/131590261