C ++ concurrency in action Reading Notes -- data sharing between threads in chapter 3 of Part 2

Source: Internet
Author: User

Tang Feng

Www.cnblogs.com/liyiwen

C ++ concurreny in action

Chapter 3 sharing data between threads

3.1 "problems" about data sharing between threads"

Invariants is damaged (for example, one read and one write)

3.1.1 race conditions

Conditional competition:

In concurrency, a race condition is anything where the outcome depends on the relative ordering of execution of operations on two or more threads; the threads race to perform their respective operations.

One of the main problems with multithreading: Race Condition

Methods To avoid race condition:

  1. Design the persistence mechanism to ensure that only one thread is modifying the data at the same time (and other threads will not access it when the data is modified) => Use mutex
  2. Use lock-free (this is in chapter 7th)
3.2 mutex in C ++ Standard

Mutex (Mutual Exclusion)

Mutex is the most commonly used data protection method in C ++, but they are not silver bullets. It is important that you organize your code to implement it correctly (appropriate granularity) for data protection, mutex also has its own problems, such as deadlocks, too many or too few protections (if the granularity is not correct, "synchronization protection is meaningless", or the overall performance of the system is deteriorated ).

Mutex basic operations: Lock () Lock, unlock () Unlock

To avoid unlock resources during various exits, C ++ provides the STD: lock_guard template and uses raiI to implement "Safe unlocking"

In the STD: lock_guard constructor, it will lock, and The Destructor will unlock

Basic example:

#include <list>#include <mutex>#include <algorithm>std::list<int> some_list; std::mutex some_mutex; void add_to_list(int new_value){    std::lock_guard<std::mutex> guard(some_mutex);     some_list.push_back(new_value);}bool list_contains(int value_to_find) {    std::lock_guard<std::mutex> guard(some_mutex);    return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();}

If the data is encapsulated (to become a private member), the data is locked during the operation and unlocked at the end of the operation, the data is generally well protected. However, this also has other problems:

Any code that has access to that pointer or reference can now access (and potentially modify) the protected data without locking the mutex.

To protect data, you must carefully design APIs so that any data can be locked before being accessed.

class some_data{    int a;    std::string b;public:    void do_something();};class data_wrapper{private:    some_data data;    std::mutex m;public:    template<typename Function>    void process_data(Function func)    {         std::lock_guard<std::mutex> l(m);        func(data);    }};some_data* unprotected;void malicious_function(some_data& protected_data){    unprotected=&protected_data;}data_wrapper x;void foo(){     x.process_data(malicious_function);     unprotected->do_something();}

A piece of code like this can easily pierce the lock protection. C ++ cannot help you with all this. Only programmers can carefully design the structure of the program. In general, we should try to observe the following conditions:

Don't pass pointers and references to protected data outside the scope of the lock, whether by returning them from a function, storing them in externally visible memory, or passing them as arguments to user-supplied functions.

This rule

3.2.3 spotting race conditions inherent in Interfaces

Race condistion. This section provides a stack example,

stack<int> s;if(!s.empty()) {    int const value=s.top();     s.pop();     do_something(value);}

In such a simple and common scenario, there is a race condition problem between multiple threads. (Even if the stack has been well protected internally)

For example, if there is only one element in statck and both threads operate on the code above, it is possible that after thread a calls empty, thread B also calls this function, as a result, both threads enter the if statement block, and the pop statement is executed twice.

In addition to this problem, there are other problems, such as multiple threads running a job, and the stack stores all the tasks, after each thread completes a job, it retrieves a new job from the stack. In this way, if the following scheduling result appears, the two threads have done the same job, no one has done the job.

Therefore, for the security that may be caused by such multithreading of the interface itself, it is necessary to make a good design and use specifications to ensure thread security.

This book provides some examples for the Design Improvement of the POP function.

Me: There are different methods for different scenarios, and there is no such thing as a rare opportunity to go all over the world. We should mainly consider whether data protection is OK in case of exceptions, what are the requirements for Thread Synchronization for continuous API calls?

3.2.4 deadlock (deadlock)

Deadlock is one of the biggest problems in multi-thread programming. The simplest deadlock is that the two threads wait for each other to release resources. As a result, no one gets the resources and freezes down there.

The simplest way to prevent deadlocks is to lock Multiple locks in the same order. But this is not a panacea for all diseases. Listing3.6 explains that if the swap function is simply implemented as "locking the two objects to be exchanged in a fixed order, and then switching the two objects, if this operation is performed in different threads and the parameter order is inconsistent, a deadlock will still occur. C ++ provides the function of "locking several locks at the same time": STD: Lock (all or nothing)

(A mutex that does permit multiple locks by the same thread is provided in the form of STD: recursive_mutex. See section 3.3.3 For details .))

class some_big_object;void swap(some_big_object& lhs,some_big_object& rhs);class X{private:    some_big_object some_detail;    std::mutex m;public:    X(some_big_object const& sd):some_detail(sd){}    friend void swap(X& lhs, X& rhs)    {        if(&lhs==&rhs)            return;        std::lock(lhs.m,rhs.m);         std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock);         std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock);         swap(lhs.some_detail,rhs.some_detail);    }};

STD: Lock (LHS. M, RHS. m); two locks can be locked simultaneously.

STD: lock_guard <STD: mutex> lock_a (LHS. m, STD: adopt_lock); STD: adopt_lock this option tells lock_guard not to lock mutex during construction, because it may have been locked before.

3.2.5 advanced "deadlock prevention" Guidelines

In addition to mutex, mutex may cause deadlocks. For example, threads wait for each other to end. In a word, "do not wait for each other"

A) avoid lock nesting

-> Do not lock a lock repeatedly.

-> Use STD: Lock to "lock Multiple locks at the same time"

B) Avoid calling externally provided locks.

Because you do not know how external functions operate locks, it is easy to have problems. Of course, when writing a library or some generalized code, you cannot avoid calling external functions. At this time, we need to formulate some rules to comply.

C) locking in a fixed order

D) Use a lock hierarchy to design the lock hierarchy on the software. To manage the lock relationship.

3.2.6 STD: unique_lock

It can also not be locked when hunting mutex, but compared to the previous STD: Adopt option, the difference is that,

  • Unique_lock can be locked later (lock_guard can no longer be locked after the adpot )??? However, unique_lock can be used, so it is more flexible at the lock time point.
  • Unique_lock can pass ownership of mutex.

Disadvantage: It is slower and larger (because the mutex is locked internally)

Example:

class X{private:    some_big_object some_detail;    std::mutex m;public:    X(some_big_object const& sd):some_detail(sd){}    friend void swap(X& lhs, X& rhs)    {        if(&lhs==&rhs)             return;        std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock);         std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock);         std::lock(lock_a,lock_b);         swap(lhs.some_detail,rhs.some_detail);    }};

3.2.7 transfer the ownership of mutext (using unique_lock)

The example is as follows:

std::unique_lock<std::mutex> get_lock(){    extern std::mutex some_mutex;    std::unique_lock<std::mutex> lk(some_mutex);    prepare_data();    return lk; }void process_data(){    std::unique_lock<std::mutex> lk(get_lock());     do_something();}

3.2.8

Pay attention to the lock granularity (including the size of the protected data and the lock as long as necessary)

Locking the mutex display during unnecessary time will affect the performance (other threads are waiting)

3.3 other synchronous protection methods

3.3.1: only synchronous protection during initialization

Some data only needs to be protected during initialization. After initialization, synchronization protection is no longer required (for example, it has become read-only). In this case, if protection is not enabled, race Condition may occur. If protection is provided, except that the first lock is necessary, subsequent access locks and unlocking will not be of any use in addition to increasing overhead.

std::shared_ptr<some_resource> resource_ptr;std::mutex resource_mutex;void foo(){    std::unique_lock<std::mutex> lk(resource_mutex);     if(!resource_ptr)    {        resource_ptr.reset(new some_resource);     }    lk.unlock();    resource_ptr->do_something();}

In order to minimize the overhead, people also think of a method similar to "double-check lock ".

void undefined_behaviour_with_double_checked_locking(){    if(!resource_ptr) // (1)    {        std::lock_guard<std::mutex> lk(resource_mutex);        if(!resource_ptr)         {            resource_ptr.reset(new some_resource); (2)        }    }    resource_ptr->do_something(); // (3)}

But there is also a problem, because there is no synchronization protection mechanism between step (1) and step (3), so it is possible to make resource_ptr not empty when a thread runs to step 2nd, but when initialization was not completed, another thread passed (1) and called (3) directly. However, because resource_ptr was not fully initialized, (3) the call result is undefined and may be an error value.

To solve this dilemma, C ++ 11 provides this method of synchronizing and protecting data during initialization.

std::shared_ptr<some_resource> resource_ptr;std::once_flag resource_flag; void init_resource(){    resource_ptr.reset(new some_resource); }void foo(){    std::call_once(resource_flag,init_resource);     resource_ptr->do_something();}

STD: call_once plus STD: once_flag can protect synchronization protection for only one operation, in addition, access in other cases after completion will not be as "overhead" as mutex lock, because it brings a balance.

class X{private:    connection_info connection_details;    connection_handle connection;    std::once_flag connection_init_flag;    void open_connection()    {        connection=connection_manager.open(connection_details);    }public:    X(connection_info const& connection_details_):    connection_details(connection_details_)    {}    void send_data(data_packet const& data)     {        std::call_once(connection_init_flag,&X::open_connection,this);         connection.send_data(data);    }    data_packet receive_data()     {        std::call_once(connection_init_flag,&X::open_connection,this);         return connection.receive_data();    }};

This example shows how to use STD: call_once and STD: once_flag to protect class members.

In addition:

class my_class;my_class& get_my_class_instance(){    static my_class instance;     return instance;}

In this usage, the initialization of my_class can be completed during the first call of the first thread in C ++ 11 (and the protection can be synchronized !! If other threads call this function before Initialization is completed, it will be stuck here ). This cannot protect thread security before C ++ 11.

3.3.2 protecting rarely updated data structures

There is also a kind of data, which requires very few write operations and most of the time is read operations. Obviously, synchronization protection is not required during read operations, the lock is only required when a write operation is performed. When multiple writes or writes are performed, other threads should read "incomplete values ". For the rarely updated data, if the general mutex is used for protection, it will be "charged" because the lock protection during read is useless. If there is a kind of lock, you can do not lock it in the case that "Each thread only performs read operations", but once there is a thread for write operations, the lock will ensure that "Once I lock it, it means that I have exclusive data, only until I unlock it, and no one can use it again, then we again achieved a balance between "security and high performance.

Unfortunately, the proposed lock was rejected in C ++ 11.

Fortunately, this lock exists in boost.

Example:

#include <map>#include <string>#include <mutex>#include <boost/thread/shared_mutex.hpp>class dns_entry;class dns_cache{    std::map<std::string,dns_entry> entries;    mutable boost::shared_mutex entry_mutex;public:    dns_entry find_entry(std::string const& domain) const    {        boost::shared_lock<boost::shared_mutex> lk(entry_mutex);         std::map<std::string,dns_entry>::const_iterator const it=        entries.find(domain);        return (it==entries.end())?dns_entry():it->second;    }    void update_or_add_entry(        std::string const& domain,        dns_entry const& dns_details)    {        std::lock_guard<boost::shared_mutex> lk(entry_mutex);         entries[domain]=dns_details;    }};

3.3.3 recursive locking

Mutex cannot lock the lock repeatedly. In general, a good design will not lock the lock repeatedly. However, if everything is necessary, you should: STD: recursive_mutex, of course, you must remember that you have to unloc several times when you lock the lock, Otherwise other threads will not be able to occupy resources.

Of course, this STD: recursive_mutex can also be the same as mutex, put in

STD: lock_guard <STD: recursive_mutex> and STD: unique_lock <STD: recursive_mutex>, which uses raiI for unlocking.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.