Practical Skills in Multithreading and Concurrency

Click the above Beginner Learning Vision“, select to add “Starred” or “Pinned

Valuable Content Delivered First Hand

Source: Zhihu user yikang Zhihu column

https://zhuanlan.zhihu.com/p/134099301

https://zhuanlan.zhihu.com/p/136861784

Part 1. Basic Concepts

1.1 Process

  • Simply understood as an execution of a program; for example, opening an application on the desktop starts a process. A process typically consists of a program, data, and a Process Control Block (PCB).

  • The traditional view of processes is that they can acquire resources allocated by the operating system, such as memory; they can participate in the operating system’s scheduling and compete for CPU time slices to gain CPU execution.

  • Creating, terminating, and switching processes incurs significant time and space overhead for the system, so the number of processes started in the system should not be too many.

1.2 Thread

  • A thread is an entity within a process, which is the basic unit that the system independently allocates and schedules. In other words, a thread is the smallest unit that can be executed and scheduled by the CPU; the process itself cannot acquire CPU time, only its threads can.

  • With the introduction of threads, unlike traditional processes: threads participate in the operating system’s scheduling, compete for CPU time slices, and gain CPU execution; while the process is responsible for acquiring resources allocated by the operating system, such as memory.

  • Threads generally do not own resources; they only possess essential resources needed during execution, and they can share all resources owned by other threads within the same process. Multiple threads within the same process can execute concurrently.

1.3 Differences and Relationships Between Threads

In addition to the concepts mentioned above, there are also:

  • Threads can be categorized into user-level threads and kernel-supported threads: user-level threads do not depend on the kernel, and the creation, termination, and switching of this type of thread do not utilize system calls; kernel-supported threads depend on the kernel, meaning that whether in user processes or in the system, their creation, termination, and switching utilize system calls.

  • System processes and user processes rely on the kernel’s process scheduling for switching. Therefore, regardless of the process type, they are related to the kernel and are switched under kernel support.

  • Processes independently occupy system resources (like memory), while threads within the same process share resources. A process itself cannot acquire CPU time; only its threads can.

Executing multiple smaller parts (threads) simultaneously within an application (process) is called multithreading. Although multiple threads share the same data, they perform different tasks.

1.4 Concurrency

Concurrency refers to the CPU executing two or more commands simultaneously at the same time. Single-core CPUs and concurrency implementations prior to C++11 are generally considered pseudo-concurrency. However, with the popularity of multi-core CPUs, C++11 began to support true concurrency.

C++11 can achieve concurrency through multithreading, which is a relatively low-level, traditional implementation method. C++11 introduces five header files to support multithreaded programming: <atomic>/<thread>/<mutex>/<condition_variable>/<future>

#include <atomic>   // C++11 atomic operations, restrict concurrent programs' use of shared data to avoid data races
#include <thread>   // This header mainly declares the std::thread class, and the std::this_thread namespace is also included in this header
#include <mutex>    // C++11 mutex. In a multithreaded environment, when multiple threads compete for the same shared resource, thread safety issues are likely to arise
#include <condition_variable>  // C++11 concurrent programming mainly contains classes and functions related to condition variables

Part 2. std::mutex Mutual Exclusion Access

<mutex> is a header file in the C++ standard library that defines some classes and methods for mutual exclusion access in the C++11 standard.

Among them, std::mutex represents a normal mutex, which can be used with std::unique_lock. When std::mutex is placed into unique_lock, the mutex will automatically lock, and when unique_lock is destructed, the mutex will be unlocked. Therefore, std::mutex can protect shared data accessed simultaneously by multiple threads, and it exclusively owns the object and does not support recursive locking on the object.

This can be understood as: each thread attempts to lock the shared resource before operating on it; only upon successfully locking can it proceed, and once the operation is complete, it unlocks. (The following image is from the internet)

Practical Skills in Multithreading and Concurrency

Common member functions include:

  1. Constructor: std::mutex does not support copy and move operations, and the initial mutex object is in an unlocked state.

  2. lock function: The mutex is locked. If a thread requests this mutex but cannot obtain it, the requesting thread will be blocked on this mutex; if it successfully obtains the mutex, it will hold it until calling unlock; if the mutex is already locked by the current calling thread, a deadlock will occur.

  3. unlock function: Unlocks the mutex, releasing the calling thread’s ownership of the mutex.

A simple example:

std::mutex mtx;  // Create a mutex
static void print_(int n, char c){  mtx.lock();       // Request access to the resource, lock it
  for (int i = 0; i < n; ++i) { std::cout << c; }
  std::cout << '\n';
  mtx.unlock();    // Operation on the resource is complete, unlock and release mutex ownership
}

Part 3. std::unique_lock Lock Management Template Class

std::unique_lock is a lock management template class that encapsulates the generic mutex.

std::unique_lock objects manage the locking and unlocking operations of mutex objects in an exclusive ownership manner (unique ownership); that is, during the lifecycle of the unique_lock object, the lock object it manages will remain locked; once the lifecycle of unique_lock ends, the lock object it manages will be unlocked. Therefore, managing a mutex object with unique_lock can serve as a return value for functions and can also be placed into STL containers.

When using the condition variable std::condition_variable, std::unique_lock must be used instead of std::lock_guard.

Common member functions include:

  1. unique_lock constructor: prohibits copy construction, allows move construction;

  2. lock function: calls the lock function of the managed mutex object;

  3. unlock function: calls the unlock function of the managed mutex object;

For example:

std::mutex mtx;   // Define a mutex
void print_thread_id(int id) {     std::unique_lock<std::mutex> lck(mtx, std::defer_lock);  // Define a lock management object (the second parameter can actually be omitted)
    lck.lock();
    std::cout << "thread #" << id << '\n';
    lck.unlock();}

Part 4. std::condition_variable Condition Variable

<condition_variable> is a header file in the C++ standard library that defines some classes and methods for condition variables in the C++11 standard.

The introduction of condition variables serves as a control structure in concurrent program design. When multiple threads access the same shared resource, not only must mutual exclusion be achieved using mutexes to avoid concurrent errors (race hazards), but after obtaining the mutex and entering the critical section, it is also necessary to check if specific conditions are met:

  1. If the condition is not met, the thread holding the mutex should release it and use the unique_lock function to block itself and hang on the thread queue of the condition variable.

  2. If the condition is met, the thread holding the mutex accesses the shared resource in the critical section, and upon exiting the critical section, it notifies (notify) the threads that are in the blocked state on the condition variable’s thread queue; the notified threads must reapply for the mutex lock.

The condition variable std::condition_variable is used for communication between multiple threads, and it can block one or multiple threads simultaneously. std::condition_variable needs to be used in conjunction with std::unique_lock.

Common member functions:

(1) Constructor: Only supports the default constructor.

(2) wait(): The current thread calling wait() will be blocked until another thread calls notify_* to wake it up. When the thread is blocked, this function will automatically call std::mutex’s unlock() to release the lock, allowing other threads that are blocked on lock competition to continue executing. Once the current thread receives a notification (notify, usually called by another thread), the wait() function automatically calls std::mutex’s lock(). There are two types of waits: unconditional blocking and condition-based blocking:

  1. Unconditional blocking: Before calling this function, the current thread should have already locked the unique_lock<mutex> lck. All threads using the same condition variable must use the same unique_lock<mutex> in the wait function. The wait function will automatically call lck.unlock() to unlock the mutex, allowing other threads blocked on the mutex to resume execution. After being awakened (notified by another thread calling the notify_* series of functions), the current thread resumes execution and automatically calls lck.lock() to lock the mutex.

  2. Condition-based blocking: The wait function sets a predicate, and the current thread will only block when the pred condition is false, and it will only be unblocked when notified by another thread if pred is true. Thus, it is equivalent to while (!pred()) wait(lck).

(3) notify_all: Wakes up all waiting threads; if there are no waiting threads, this function does nothing.

(4) notify_one: Wakes up a certain waiting thread; if there are no waiting threads, this function does nothing; if multiple waiting threads exist simultaneously, waking one thread is unspecified.

In simple terms, when a wait function of a std::condition_variable object is called, it uses std::unique_lock (through std::mutex) to lock the current thread. The current thread will remain blocked until another thread calls a notification function on the same std::condition_variable object to wake it up.

Part 5. std::atomic Atomic Operations

<atomic> is a header file in the C++ standard library that defines classes and methods for atomic operations in the C++11 standard, specifically for thread and concurrency control.

The main characteristic of atomic operations is that there is no data race during concurrent access to atomic objects; using atomic objects can achieve lock-free designs for data structures. During multithreaded concurrent execution, atomic operations are uninterrupted execution segments.

(1) atomic_flag class

  1. It is a simple atomic boolean type that only supports two operations: test_and_set(flag=true) and clear(flag=false).

  2. Unlike all other specialized classes of std::atomic, it is lock-free.

  3. Combining std::atomic_flag::test_and_set() and std::atomic_flag::clear(), std::atomic_flag objects can be used as simple spin locks.

  4. atomic_flag only has a default constructor, and disables copy and move constructors.

  5. If the macro ATOMIC_FLAG_INIT is not explicitly used for initialization during creation, the state of the newly created std::atomic_flag object is unspecified, meaning it has neither been set nor cleared; if initialized with this macro, the std::atomic_flag object is in a clear state upon creation.

  • test_and_set: Returns the current state of the std::atomic_flag object, checking if the flag is set; if set, it returns true directly; if not set, it sets the flag to true and then returns false. This function is atomic.

  • clear: Clears the flag of the std::atomic_flag object, setting its value to false.

(2) std::atomic class

  1. std::atomic provides specialized implementations for boolean, integral, and pointer types. Each instantiation and full specialization of std::atomic defines an atomic type.

  2. If one thread writes to an atomic object while another thread reads from it, the behavior is well-defined.

  3. Access to atomic objects can establish inter-thread synchronization as specified by std::memory_order and order non-atomic memory accesses.

  4. std::atomic can be instantiated with any trivially copyable type T.

  5. std::atomic is neither copyable nor movable.

  6. ATOMIC_VAR_INIT(val): This macro can be used to initialize std::atomic objects directly through the constructor.

Common member functions of std::atomic:

  1. std::atomic::store(val) function copies the parameter val to the value encapsulated by the atomic object.

  2. std::atomic::load() reads the value encapsulated by the atomic object.

  3. std::atomic::exchange(val) reads and modifies the encapsulated value; exchange replaces the previously encapsulated value with the value specified by val and returns the previously encapsulated value; the entire process is atomic.

  4. atomic() default constructor creates a std::atomic object that is in an uninitialized state; the uninitialized std::atomic object can be initialized by the atomic_init function.

  5. atomic (T val) is an initialization constructor that initializes a std::atomic object with type T.

  6. atomic (const atomic&) copy constructor is disabled.

References:

1. blog.csdn.net/liuxuejia

2. blog.csdn.net/fengbingc

3. cnblogs.com/wangshaowei

4. blog.csdn.net/fengbingc

5. cnblogs.com/taiyang-li/

6. blog.csdn.net/tanningzh

If there are any copyright issues with this article, please contact the author through the backend, and it will be deleted on its own.

Good news!
The Beginner Learning Vision knowledge group is now open to the public 👇👇👇



Download 1: OpenCV-Contrib Extension Module Chinese Version Tutorial
Reply "Extension Module Chinese Tutorial" in the "Beginner Learning Vision" WeChat public account's backend to download the first OpenCV extension module tutorial in Chinese, covering installation of extension modules, SFM algorithms, stereo vision, object tracking, biological vision, super-resolution processing, and more than twenty chapters of content.

Download 2: Python Vision Practical Projects 52 Lectures
Reply "Python Vision Practical Projects" in the "Beginner Learning Vision" WeChat public account's backend to download 31 vision practical projects including image segmentation, mask detection, lane line detection, vehicle counting, eyeliner addition, license plate recognition, character recognition, emotion detection, text content extraction, and face recognition, to help quickly learn computer vision.

Download 3: OpenCV Practical Projects 20 Lectures
Reply "OpenCV Practical Projects 20 Lectures" in the "Beginner Learning Vision" WeChat public account's backend to download 20 practical projects based on OpenCV for advanced learning of OpenCV.

Group Chat

Welcome to join the public account reader group to communicate with peers; currently, there are WeChat groups on SLAM, 3D vision, sensors, autonomous driving, computational photography, detection, segmentation, recognition, medical imaging, GAN, algorithm competitions, etc. (these will be gradually subdivided). Please scan the WeChat ID below to join the group, and note: "Nickname + School/Company + Research Direction"; for example: "Zhang San + Shanghai Jiao Tong University + Vision SLAM". Please follow the format; otherwise, it will not be approved. After successful addition, you will be invited to related WeChat groups based on your research direction. Please do not send advertisements in the group, or you will be removed from the group. Thank you for your understanding~



Leave a Comment