C++多线程中共享变量同步问题
多线程共享变量同步1、互斥量(1)std::mutex(2)std::recursive_mutex(3)std::timed_mutex2、锁管理器(1)std::lock_guardlk(2)std::unique_locklk(3)std::unique_lock第二个参数使用3、条件变量(1)std::condition_variablea>wait()函数b>notify_one()函数
目录
1、互斥量
(1)std::mutex
std::mutex是互斥量类。
常用函数:
a>lock()函数
b>unlock()函数
c>try_lock()函数
try_lock尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。调用情况有以下两种:
(1)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互 斥量。
(2)如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉,继续往下执行。
用法:同一个线程中同一个互斥量对象只能调用一次lock函数,否则程序会报错,lock()的次数和unlock()需要相等。用法参考如下
mtx.lock();
//todo 共享变量处理
mtx.unlock();
try_lock的demo如下:
#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;
}
执行结果如下:
(2)std::recursive_mutex
std::recursive_mutex 递归互斥量类型。
常用函数:
a>lock()函数
b>unlock()函数。
用法:可以在同一个线程中队同一个互相量对象进行多次的lock()和unlock()。lock()的次数和unlock()需要相等。该互斥类型由于多次lock锁,所以性能较差。
完整的demo可参考如下:
#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;
}
执行结果如下:
(3)std::timed_mutex
std::timed_mutex是关于时间类型的互斥量类型,比mutex多的成员函数如下:
a> bool try_lock_for(const chrono::duration<_Rep, _Period>& __rtime)
设置一个时间rtime范围,在这一段时间范围之内线程如果没有获得锁,则保持阻塞,如果获取锁,该函数返回true;如果超时rtime时间范围,为获取锁,则返回false。
b> bool try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime)
函数参数__atime表示一个时刻,在这一时刻之前线程如果没有获得锁,则保持阻塞,如果获得锁,则返回true.如果超过指定时刻__atime没有获得锁,则函数返回false。
try_lock_for的demo如下:
#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;
}
执行结果如下:
try_lock_until的demo如下:
#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;
}
执行结果如下:
死锁:两个锁及以上才会出现。如果多个锁的获取设计不当就出现一直获取锁的阻塞状态,使线程永远拿不到锁。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、锁管理器
锁管理器是遵循RAII机制,RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
(1)std::lock_guard<std::mutex>lk
std::lock_guard是模板类,是std::mutex的lock和unlock功能集成,在创建std::lock_guard对象时,会自动lock加锁,不需要手动释放锁(unlock),这样可以避免忘记解锁。但std::lock_guard对象生命周期过程中不能解锁,必须得生命周期结束时,会自动析构解锁。
(2)std::unique_lock<std::mutex>lk
std::unqiue_lock是模板类,是std::lock_gurad的升级版,它除了拥有std::lock_gurad自动析构的功能,它可以随时解锁,比std::lock_gurad使用更灵活。
std::lock_guard的 demo如下:
#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;
}
执行结果如下:
std::unique_lock的demo如下:
#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;
}
执行结果如下:
上面的demo只是简单演示,具体的使用需要根据场景灵活应用。
(3)std::unique_lock第二个参数使用
可以在创建锁管理器对象时,通过设置第二个参数来进行获取锁,具体的参数含义如下:
std::lock_guard的第二个参数只有一个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);
}
第二个参数使用可以参考
c++多线程,adopt_lock_t/defer_lock_t/try_to_lock_t_lysSuper的博客-CSDN博客
c++11 std::defer_lock, std::try_to_lock, std::adopt_lock__李白_的博客-CSDN博客
3、条件变量
(1)std::condition_variable
std::condition_variable是一个类,搭配锁管理器std::unqiue_lock和互斥量std::mutex来使用,主要利用成员函数wait()、notify_one()、notify_all()函数来实现线程间执行顺序控制。
成员函数
a>wait()函数
wait函数有两种,如下:
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时,才获取锁,继续往下执行。
第一种wait会存在虚假唤醒,第二种wait会避免虚假唤醒,所以应使用带2个参数的wait。
b>notify_one()函数
通知一个线程的wait()
c>notify_all()函数
通知所有线程的wait()
demo 如下:
#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如下:
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})
执行结果如下:
上面的demo 利用条件变量实现了一个生产者消费者的功能。
4、原子操作
原子操作是颗粒度最小的操作,只会存在执行完成,执行未完成两种状态。所以应用在多线程中可以实现共享变量可以保证读写的互斥性和提高执行效率,原子操作的执行效率是比锁、锁管理器高好几倍。使用时需要注意不是原子操作的运算符不要使用,以免结果异常,可以通过写demo进行测试。
注意:如果原子共享变量,在变量的读写顺序上有要求,需要注意store写函数中的第二个参数std::memory_order的选择,推荐memory_order_seq_cst。
可以参考:
c++11 atomic 之 atomic 使用_atomic_init_Npgw的博客-CSDN博客
C++11多线程(五)原子操作简单使用_c++ atomic load_AczQc的博客-CSDN博客
C++ 并发指南-atomic原子变量使用struct(一)_c++原子变量用法_面-包的博客-CSDN博客
C++高并发:原子操作和memory order - 知乎 (zhihu.com)
附加:
多线程创建函数可参考:C++多线程编程(真实入门)_c++函数放入线程中执行_夜雨听萧瑟的博客-CSDN博客
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)