"Modern C++ Tutorial" Notes (5-7)

5 Smart Pointers and Memory Management

5.1 RAII and reference counting

In traditional C++, "remembering" to release resources manually is not always a best practice. Because we are likely to forget to release resources and cause leaks. So the usual practice is that for an object, we apply for space in the constructor, and release the space in the destructor (called when leaving the scope), which is what we often call RAII resource acquisition and initialization technology .

There are exceptions to everything. We always have the need to allocate objects on free storage. In traditional C++, we have to use new and delete to "remember" to release resources. C++11 introduces the concept of smart pointers and uses the idea of ​​reference counting, so that programmers no longer need to care about manually releasing memory. These smart pointers include std::shared_ptr/std::unique_ptr/std::weak_ptr, and using them requires including the header file memory.

5.2 std::shared_ptr

std::shared_ptr is a smart pointer that can record how many shared_ptr points to an object, thereby eliminating the need to explicitly call delete, and the object will be automatically deleted when the reference count becomes zero.

But it's not enough, because using std::shared_ptr still needs to use new to call, which makes the code somewhat asymmetric.

std::make_shared can be used to eliminate the explicit use of new, so std::make_shared will allocate and create the object in the incoming parameter, and return the std::shared_ptr pointer of this object type.

5.3 std::unique_ptr

std::unique_ptr is an exclusive smart pointer, which prohibits other smart pointers from sharing the same object with it, thus ensuring the safety of the code.

Since it is exclusive, in other words, it cannot be copied. However, we can use std::move to transfer it to other unique_ptr.

5.4 std::weak_ptr

std::weak_ptr is a weak reference (in comparison, std::shared_ptr is a strong reference). Weak references do not cause the reference count to increase.

std::weak_ptr has no * operator and -> operator, so it cannot operate on resources. It can be used to check whether std::shared_ptr exists, and its expire() method can return false when the resource is not released. , otherwise return true; in addition, it can also be used to obtain a std::shared_ptr pointer pointing to the original object, and its lock() method returns a std::shared_ptr pointer pointing to the original object when the original object is not released , and then access the resources of the original object, otherwise return nullptr.

6 regular expressions

There is nothing to introduce in this chapter, skip it.

7 Parallelism and Concurrency

7.1 Parallel Basics

std::thread is used to create an executing thread instance, so it is the basis of all concurrent programming, and header files need to be included when using it. It provides many basic thread operations, such as get_id() to obtain the thread ID of the created thread , use join() to wait for a thread to finish (join with that thread), and so on.

7.2 Mutex and critical section

We have learned the basics of concurrency technology in the operating system or database knowledge, and mutex is one of the cores. C++11 introduces mutex-related classes, and all related functions are placed in the mutex header file.

std::mutex is the most basic mutex class in C++11. A mutex can be created by instantiating std::mutex, and can be locked and unlocked by its member function lock(). But in the process of actually writing code, it is best not to call member functions directly, because calling member functions requires calling unlock() at the exit of each critical section, and of course, exceptions are also included. At this time, C++11 also provides a RAII syntax template class std::lock_guard for mutexes. RAII guarantees the exception safety of the code without losing the simplicity of the code.

Since C++ guarantees that all stack objects will be destroyed at the end of their lifetime, such code is also exception-safe. Regardless of whether critical_section() returns normally or throws an exception in the middle, it will cause a stack rollback, and unlock() will be called automatically.

And std::unique_lock appears relative to std::lock_guard, std::unique_lock is more flexible, the object of std::unique_lock will have exclusive ownership (no other unique_lock object owns the ownership of a mutex object at the same time) Manages locking and unlocking operations on mutex objects. So in concurrent programming, it is recommended to use std::unique_lock.

std::lock_guard cannot explicitly call lock and unlock, but std::unique_lock can be called at any position after the declaration, which can reduce the scope of the lock and provide higher concurrency.

If you use the condition variable std::condition_variable::wait you must use std::unique_lock as a parameter.

7.3 Futures

Futures are represented by std::future, which provides a way to access the results of asynchronous operations. This sentence is very difficult to understand. In order to understand this feature, we need to understand the multithreading behavior before C++11.

Just imagine, if our main thread A wants to create a new thread B to perform a certain task we expect, and return me a result. At this time, thread A may be busy with other things and has no time to consider the results of B, so we naturally hope to get the results of thread B at a certain time.

Before C++11's std::future was introduced, the usual approach was: create a thread A, start task B in thread A, send an event when the preparation is complete, and save the result in a global variable. And the main function thread A is doing other things. When the result is needed, a thread waiting function is called to obtain the execution result.

The std::future provided by C++11 simplifies this process and can be used to obtain the results of asynchronous tasks. Naturally, we can easily imagine it as a simple means of thread synchronization, namely barriers.

7.4 Condition variables

The condition variable std::condition_variable was born to solve the deadlock, and it was introduced when the mutual exclusion operation was not enough. For example, a thread may need to wait for a certain condition to be true before continuing to execute, and a busy-wait loop may prevent all other threads from entering the critical section so that when the condition is true, a deadlock occurs. Therefore, the condition_variable instance is created mainly to wake up the waiting thread to avoid deadlock. notify_one() of std::condition_variable is used to wake up a thread; notify_all() is to notify all threads.

It is worth mentioning that although we can use notify_one() in the producer, it is actually not recommended to use it here, because in the case of multiple consumers, our consumer implementation simply abandons the lock holding Yes, this makes it possible to let other consumers compete for this lock, thus making better use of concurrency among multiple consumers. Having said that, in fact, because of the exclusiveness of std::mutex, we can't expect multiple consumers to consume the content produced in the queue in a true sense, and we still need a more granular approach.

7.5 Atomic operations and the memory model

Guess you like

Origin blog.csdn.net/YuhsiHu/article/details/131987090