Synchronization problem of shared variables in C++ multithreading

Table of contents

       1. Mutex

(1)std::mutex

(2)std::recursive_mutex 

(3)std::timed_mutex

2. Lock manager

(1)std::lock_guardlk

(2)std::unique_locklk

(3) The second parameter of std::unique_lock is used

3. Condition variables

(1)std::condition_variable

   a>wait() function

   b>notify_one() function 

   c>notify_all() function

4. Atomic operations


1. Mutex

(1)std::mutex

       std::mutex is a mutex class.

Common functions:

  a>lock() function

  b>unlock() function

  c>try_lock() function

  try_lock tries to lock the mutex. If the mutex is occupied by other threads, the current thread will not be blocked. There are two calling situations:
(1) If the current mutex is not occupied by other threads, the thread locks the mutex until the thread calls unlock to release the mutex.
(2) If the current mutex is locked by other threads, the current calling thread returns false, and will not be blocked, and continue to execute .

Usage: The same mutex object in the same thread can only call the lock function once, otherwise the program will report an error, and the number of lock() and unlock() needs to be equal. The usage reference is as follows

    mtx.lock();
    //todo 共享变量处理
    mtx.unlock();

 The demo of try_lock is as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <thread>
std::mutex mtx;
int gCnt = 0;
void Threadfun()
{
    while(true)
    {
        if(mtx.try_lock())
        {
            gCnt++;
            std::cout << "get lock " << std::endl;
            mtx.unlock();
        }else
        {
            gCnt--;
            std::cout << "can't get lock " << std::endl;
        }

    }

}

void Threadfun2()
{
    while(true)
    {

        mtx.lock();
        std::this_thread::sleep_for(std::chrono::microseconds(3000));
        mtx.unlock();
    }
}
int main()
{
    std::thread th1(Threadfun);
    std::thread th2(Threadfun2);
    th1.join();
    th2.join();
    std::cout << "gCnt " << gCnt << std:: endl;

    return 0;
}

 The execution results are as follows:

(2)std::recursive_mutex 

std::recursive_mutex The recursive mutex type.

 Common functions:

  a>lock() function

  b>unlock() function.

Usage: You can perform multiple lock() and unlock() on the same mutual variable object in the same thread. The number of lock() and unlock() need to be equal. This mutex type has poor performance due to multiple locks.

The complete demo can be referred to as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <thread>
std::recursive_mutex recur_mtx;
int gCnt = 0;
void Threadfun()
{
    for(int i = 0;i < 10000;i++)
    {
        recur_mtx.lock();
        gCnt++;
        recur_mtx.unlock();
    }

}

void Threadfun2()
{

   recur_mtx.lock();
   Threadfun();
   recur_mtx.unlock();

}

int main()
{
    std::thread th(Threadfun2);
    th.join();
    std::cout << "gCnt " << gCnt << std:: endl;

    return 0;
}

The execution results are as follows:

(3)std::timed_mutex

std::timed_mutex is a mutex type of time type, and the member functions more than mutex are as follows:

a> bool try_lock_for(const chrono::duration<_Rep, _Period>& __rtime)

Set a time rtime range. If the thread does not acquire the lock within this period of time, it will remain blocked. If the lock is acquired, the function returns true; if the rtime time range is exceeded and the lock is acquired, false is returned.

b> bool try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime)
The function parameter __atime indicates a moment. If the thread does not acquire the lock before this moment, it will remain blocked. If it acquires the lock, it will return true. If If the lock is not acquired beyond the specified time __atime, the function returns false.

The demo of try_lock_for is as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <shared_mutex>
#include <thread>
std::timed_mutex time_mtx;
int gCnt = 0;
void ThreadTime1()
{
    time_mtx.lock();
    std::this_thread::sleep_for(std::chrono::microseconds(2000));
    time_mtx.unlock();
}
void ThreadTime()
{
    std::chrono::microseconds dur(1000);
    if(time_mtx.try_lock_for(dur))
    {
        gCnt +=10;
    }else
    {
        gCnt +=20;
    }

    std::cout << "ThreadTime run done" << std::endl;
}
int main()
{
    std::thread th1(ThreadTime1);
    std::thread th2(ThreadTime);
    th1.join();
    th2.join();
    std::cout << "gCnt " << gCnt << std:: endl;

    return 0;
}

 The execution results are as follows:

 The demo of try_lock_until is as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <shared_mutex>
#include <thread>
std::timed_mutex time_mtx;
int gCnt = 0;
void ThreadTime1()
{
    time_mtx.lock();
    std::this_thread::sleep_for(std::chrono::microseconds(1000));
    time_mtx.unlock();
}
void ThreadTime()
{
    std::chrono::microseconds dur(1000);
    if(time_mtx.try_lock_until(std::chrono::steady_clock::now()+dur))
    //当前时间+1s时间内获取到锁,执行+10操作,如果超时为获取执行+20操作
    {
        gCnt +=10;
    }else
    {
        gCnt +=20;
    }

    std::cout << "ThreadTime run done" << std::endl;
}
int main()
{
    std::thread th1(ThreadTime1);
    std::thread th2(ThreadTime);
    th1.join();
    th2.join();
    std::cout << "gCnt " << gCnt << std:: endl;
    return 0;
}

The execution results are as follows:

 

Deadlock: two or more locks will occur. If the acquisition of multiple locks is improperly designed, there will be a blocking state of acquiring locks all the time, so that the thread will never be able to obtain the lock. eg:

std::mutex mtx1;
std::mutex mtx2;
void threadF1()
{
    mtx1.lock();
    mtx2.lock();     //线程1等待mtx2的锁
    //to do
    mtx1.unlock();
    mtx2.unlock();
}

void threadF2()
{
    mtx2.lock();
    mtx1.lock();    //线程2等待mtx1的锁
    //to do
    mtx1.unlock();
    mtx2.unlock();
}

2. Lock manager

The lock manager follows the RAII mechanism. RAII ( Resource A acquisition  II initialization) was proposed by Bjarne Stroustrup, the father of c++. The Chinese translation is resource acquisition and initialization. He said: The technology of using local objects to manage resources is called  Resource acquisition is initialization; the resources here mainly refer to limited things in the operating system such as memory, network sockets, etc., and local objects refer to objects stored on the stack. Its life cycle is managed by the operating system. Manual intervention.

(1)std::lock_guard<std::mutex>lk

std::lock_guard is a template class that integrates the lock and unlock functions of std::mutex. When creating a std::lock_guard object, it will automatically lock and lock, and there is no need to manually release the lock (unlock), which can avoid forgetting to unlock. However, the std::lock_guard object cannot be unlocked during the life cycle, and it must be automatically destructed and unlocked when the life cycle ends.

(2)std::unique_lock<std::mutex>lk

std::unqiue_lock is a template class and an upgraded version of std::lock_gurad. In addition to the automatic destruction function of std::lock_gurad, it can be unlocked at any time, which is more flexible than std::lock_gurad.

The demo of std::lock_guard is as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <thread>
#include <sys/time.h>
#include <cmath>
std::mutex mtx;
int gCnt = 0;
struct timeval start,end;
void Threadfun()
{

    std::lock_guard<std::mutex>lk(mtx);
    gCnt++; //共享操作已操作完成
    std::cout << "gCnt " << gCnt << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(10));  //相当于处理其他代码


}

void Threadfun2()
{

    std::lock_guard<std::mutex>lk(mtx);
    gCnt += 10;
    std::cout << "gCnt " << gCnt << std::endl;
    gettimeofday(&end,nullptr);

    std::cout << "use time is " << end.tv_sec + end.tv_usec * pow(10,-6)-start.tv_sec-start.tv_usec*pow(10,-6);
    std::cout <<" s" << std::endl;
}

int main()
{

    gettimeofday(&start,nullptr);

    std::thread th1(Threadfun);
    std::thread th2(Threadfun2);
    th1.join();
    th2.join();
    std::cout << "gCnt " << gCnt << std:: endl;

    return 0;
}

The execution results are as follows:

 The demo of std::unique_lock is as follows:

#include <iostream>
#include <unistd.h>
#include <mutex>
#include <thread>
#include <sys/time.h>
#include <cmath>
std::mutex mtx;
int gCnt = 0;
struct timeval start,end;
void Threadfun()
{

    std::unique_lock<std::mutex>lk(mtx);
    gCnt++; //共享操作已操作完成
    std::cout << "gCnt " << gCnt << std::endl;
    lk.unlock();
    std::this_thread::sleep_for(std::chrono::seconds(10));  //相当于处理其他代码


}

void Threadfun2()
{

    std::unique_lock<std::mutex>lk(mtx);
    gCnt += 10;
    std::cout << "gCnt " << gCnt << std::endl;
    gettimeofday(&end,nullptr);

    std::cout << "use time is " << end.tv_sec + end.tv_usec * pow(10,-6)-start.tv_sec-start.tv_usec*pow(10,-6);
    std::cout <<" s" << std::endl;
}

int main()
{

    gettimeofday(&start,nullptr);

    std::thread th1(Threadfun);
    std::thread th2(Threadfun2);
    th1.join();
    th2.join();
    std::cout << "gCnt " << gCnt << std:: endl;

    return 0;
}

 The execution results are as follows:

 The demo above is just a simple demonstration, and the specific use needs to be flexibly applied according to the scene.

(3) The second parameter of std::unique_lock is used

You can acquire the lock by setting the second parameter when creating the lock manager object. The meaning of the specific parameters is as follows:

 The second parameter of std::lock_guard has only one adopt_lock.

std::mutex mtx;
void Threadfun()
{

    std::lock_guard<std::mutex>lk1(mtx,std::adopt_lock);
    
    std::unique_lock<std::mutex>lk2(mtx,std::adopt_lock);
    std::unique_lock<std::mutex>l3(mtx,std::try_to_lock);
    std::unique_lock<std::mutex>lk5(mtx,std::defer_lock);
}

 The second parameter can be used for reference

C++ multithreading, adopt_lock_t/defer_lock_t/try_to_lock_t_lysSuper's Blog - CSDN Blog

c++11 std::defer_lock, std::try_to_lock, std::adopt_lock__Li Bai_'s Blog-CSDN Blog

3. Condition variables

(1)std::condition_variable

std::condition_variable is a class that is used with the lock manager std::unqiue_lock and the mutex std::mutex. It mainly uses the member functions wait(), notify_one(), and notify_all() to realize the execution sequence control between threads. .

member function

a>wait() function

There are two types of wait functions, as follows:

1、void wait(unique_lock<mutex>& __lock) noexcept;
  //阻塞当前线程,等待notify_one、notify_all的唤醒后,获取锁,继续往下执行。


2、void wait(unique_lock<mutex>& __lock, _Predicate __p)
    //阻塞当前线程,等待notify_one、notify_all的唤醒,且_Predicate 判断式返回true时,才获取锁,继续往下执行。

The first type of wait will have false wakeups, and the second type of wait will avoid false wakeups, so wait with 2 parameters should be used.

b>notify_one() function 

Notify a thread's wait()

c>notify_all() function

 Notify all threads of wait()

The demo is as follows:

#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <sys/time.h>
#include <cmath>
#include <condition_variable>
#include <vector>

unsigned long counter=0;
unsigned long long VarMax = pow(10,2);
std::vector<int>vecQue;

std::mutex mtx;
std::condition_variable cnd;
#define CONDITION
//#define MUT
void Increase(unsigned long long nCnt)
{
    std::cout << "Increase thread id  " << std::this_thread::get_id() << std::endl;
    for(unsigned long long i =0; i < nCnt;i++)
    {
#ifdef MUT
        mtx.lock();
        counter++;
        mtx.unlock();
#endif

#ifdef CONDITION
        std::unique_lock<std::mutex>lk(mtx);
        vecQue.emplace_back(i);
        cnd.notify_one();
#endif
    }
}

void OutVarFunc()
{
    while (true)
    {
#ifdef MUT
    mtx.lock();
    std::cout << "OutVarFunc  " << counter << std::endl;
    mtx.unlock();
    
#endif

#ifdef CONDITION
#if 0
    std::unique_lock<std::mutex>lk(mtx);
    cnd.wait(lk);
    int tmp = vecQue.front();
    std::cout << "OutVarFunc  " << tmp  << " size " << vecQue.size() << std::endl;
    vecQue.erase(vecQue.begin());
#else
    std::unique_lock<std::mutex>lk(mtx);
    cnd.wait(lk,[](){
        if(!vecQue.empty())
        {
            return true;
        }else{
            return false;
        }
    });

    int tmp = vecQue.front();
    std::cout << "OutVarFunc  " << tmp  << " size " << vecQue.size() << std::endl;
    vecQue.erase(vecQue.begin());
    #endif
#endif
    }

}

int main(int argc,char** argv)
{
    std::cout << "main thread id " << std::this_thread::get_id() << std::endl;

    struct timeval start,end;
    gettimeofday(&start,nullptr);

    std::thread t3(OutVarFunc);
    std::thread t1(Increase,VarMax);
    std::thread t2(Increase,VarMax);
   
    t1.join();
    t2.join();
    t3.join();
    
    gettimeofday(&end,nullptr);

    std::cout << "use time is " << end.tv_sec + end.tv_usec * pow(10,-6)-start.tv_sec-start.tv_usec*pow(10,-6);
    std::cout <<"s, counter " << counter << std::endl;

    return 0;
}

CMakeLists.txt is as follows:

cmake_minimum_required(VERSION 3.0)
project(MAIN)

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -pthread")

add_definitions("-Wall -g")

file(GLOB_RECURSE SRC_CPP ${PROJECT_SOURCE_DIR}/src/*.cpp)

add_executable(${PROJECT_NAME} ${SRC_CPP})

The execution results are as follows:

 

The above demo uses condition variables to realize the function of a producer consumer. 

4. Atomic operations

Atomic operations are the smallest granular operations, and there are only two states: execution completed and execution incomplete. Therefore, shared variables can be implemented in multi-threaded applications to ensure mutual exclusion of reads and writes and improve execution efficiency. The execution efficiency of atomic operations is several times higher than that of locks and lock managers. When using it, you need to pay attention not to use operators that are not atomic operations, so as to avoid abnormal results. You can test it by writing a demo.

Note: If atomic shared variables have requirements on the read and write order of variables, you need to pay attention to the selection of the second parameter std::memory_order in the store write function, and memory_order_seq_cst is recommended.

You can refer to:

C++11 atomic uses atomic_atomic_init_Npgw's blog - CSDN Blog

C++11 multithreading (five) simple use of atomic operations_c++ atomic load_AczQc's Blog-CSDN Blog

C++ Concurrency Guide - Atomic Atomic Variables Using struct (1)_C++ Atomic Variable Usage_Face-Package Blog-CSDN Blog

 C++ high concurrency: atomic operations and memory order - Zhihu (zhihu.com)

Additional :

Multi-thread creation function can refer to: C++ multi-thread programming (real introduction)_C++ function is executed in the thread_Yuyu listen to Xiaose's blog-CSDN blog

Guess you like

Origin blog.csdn.net/hanxiaoyong_/article/details/130661824