- 上一篇文章介绍了 C++多线程如何创建,这篇文章对C++多线程同步方式做一个总结
-
互斥和同步的区别:互斥是对共同资源的互斥访问,访问是乱序的,同步就是协调步调,按照一定顺序执行。同步一般已经实现了互斥,通过条件设置实现顺序访问。
-
C语言线程互斥和同步方式,在文章 C语言多线程同步方式一,C语言多线程同步方式二中已经做了介绍且有实例。
-
C语言线程同步的常见方式:
1. 互斥锁 2. 条件变量 3. 读写锁 4. 自旋锁 5. 信号量 6. 关卡
以下着重介绍几种常用的C++线程同步方式,包括互斥锁、条件变量、信号量
-
c++中互斥锁的使用比c复杂一些。c++可使用的互斥锁相关的类有
1. std::mutex 2. std::lock 3. std::lock_guard 4. std::unique_lock 5. std::scoped_lock
#include
#include
#include
#include
std::mutex myMutex;
int num = 0;
void PrintNum(std::string str)
{
while (num < 10)
{
myMutex.lock(); // 加锁
std::cout << str << " num = " << num << std::endl;
num++;
myMutex.unlock(); // 解锁
}
}
int main()
{
std::thread threadOne(PrintNum, "thread one");
std::thread threadTwo(PrintNum, "thread two");
threadOne.join();
threadTwo.join();
return 0;
}
// 编译运行
g++ -o main main.cpp -pthread
这种需要手动解锁的都有忘记解锁的风险
2.1.2 使用实例:std::lock- std::lock通常不直接使用,而且使用条件也比较局限:“加锁动作不是分散的,而是要同时加锁”。可配合mutex/lock_guard/unique_lock一起使用
- std::lock可以用来锁住两个或两个以上的互斥量或对象,且内部有免死锁算法避免死锁。若有任何一个不可用则把已锁定的释放掉然后阻塞等待,函数正常返回表示对象均已加锁
- std::lock不会自动解锁,一般需要锁定互斥量或对象辅助进行解锁 *** 作。互斥量的话就直接mutex.unlock,lock_guard/unique_lock对象则离开作用域自动解锁
- std::lock与std::scoped_lock的区别见:使用实例 std::scoped_lock
- 配合mutex使用
int num = 0;
std::mutex mutex1;
std::mutex mutex2;
void safe_increment()
{
/*同时锁两个*/
std::lock(mutex1, mutex2);
for (int i = 0; i < 10000000; i++)
{
++num;
}
/*需要手动解锁*/
std::cout << std::this_thread::get_id() << ": " << num << '\n';
mutex1.unlock();
mutex2.unlock();
}
- 配合lock_guard使用
std::mutex mutex1;
std::mutex mutex2;
std::lock(mutex1, mutex2); // 参数为互斥量,同时锁定两个互斥量
std::lock_guard lock1(mutex1, std::adopt_lock);
std::lock_guard lock2(mutex2, std::adopt_lock);
lock_guard使用adopt_lock参数表示这个互斥量已经被锁定了,不需要在构造函数中再加锁了
- 配合unique_lock使用
std::mutex mutex1;
std::mutex mutex2;
std::unique_lock lock1(mutex1, std::defer_lock);
std::unique_lock lock2(mutex2, std::defer_lock);
std::lock(lock1, lock2); // 参数为对象,同时锁定lock1和lock2两个对象
// 如果不需要两个同时锁定,也可以各自锁定
lock1.lock();
lock2.lock();
unique_lock中使用了defer_lock参数,表示加锁的动作可以留到后面进行,可能是加锁之前还有一些 *** 作
- std::lock是阻塞等待,std::try_lock是非阻塞等待,如果没有获取到锁,则不修改受保护对象,同时获取到锁返回-1可修改受保护对象。示例如下
std::mutex mutex1;
std::mutex mutex2;
void Function()
{
while (true) {
int result = std::try_lock(mutex1, mutex2);
if (result == -1) {
// do something
mutex1.unlock();
mutex2.unlock();
}
}
}
2.1.3 使用实例:std::lock_guard
- 为避免mutex忘记unlock的出现,c11中引入了lock_guard,比较常用
- std::lock_guard对象构造时加锁,对象析构时解锁。作用域内生效
- std::lock_guard可与mutex互斥量配合使用,使用时不要手动unlock,防止析构函数再unlock时报异常。不可复制
- 上锁不成功则阻塞等待
while (ros::ok()) {
{
std::mutex mutexVar,
std::lock_guard<std::mutex> lockGuard(mutexVar);
// do something
} // 离开作用域自动解锁
...
}
2.1.4 使用实例:std::unique_lock
- std::unique_lock对象构造时加锁,对象析构时解锁。作用域内生效
- std::unique_lock可与mutex互斥量配合使用,可灵活加解锁
- std::unique_lock可移动(可转移互斥量的所有权)不可复制
- 上锁不成功则阻塞等待,使用try_to_lock则表示非阻塞
while (ros::ok()) {
{
std::mutex mutexVar,
std::unique_lock<std::mutex> uniqueLock(mutexVar);
if (!condition) {
uniqueLock.unlock(); // 手动解锁
continue;
}
// do something
} // 离开作用域自动解锁
...
}
- std::unique_lock还有try_to_lock、owns_lock等成员函数。调用try_to_lock上锁不成功不会阻塞在那里,此时可以去 *** 作不受保护的数据
list<int> myList; // 受保护对象
mutex myMutex;
void Function()
{
for (int num = 0; num < 10000; num++) {
std::unique_lock<std::mutex> my_unique(myMutex, std::try_to_lock);
if(my_unique.owns_lock()){
cout<<"插入数据: "<<num<<endl;
myList.push_back(num);
}
else{
cout<<"没能拿到锁,只能干点别的事"<<endl;
}
}
}
2.1.5 使用实例:std::scoped_lock
- c++17中才支持
- std::scoped_lock内部封装了std::lock,同时析构函数调用了unlock。可在对象构造时加锁,对象析构时解锁,作用域内生效。同时内部也有免死锁算法避免死锁
- std::scoped_lock可对一个或多个互斥量加解锁
- std::scoped_lock不可复制也不可移动
std::mutex mutex1;
std::mutex mutex2;
std::scoped_lock scopedLock(mutex1, mutex2);
// 相当于
std::lock(mutex1, mutex2); // 参数为互斥量,同时锁定两个互斥量
std::lock_guard lock1(mutex1, std::adopt_lock);
std::lock_guard lock2(mutex2, std::adopt_lock);
// 或者相当于
std::unique_lock lock1(mutex1, std::defer_lock);
std::unique_lock lock2(mutex2, std::defer_lock);
std::lock(lock1, lock2); // 参数为对象,同时锁定lock1和lock2两个对象
2.2 条件变量
- 条件变量通常结合unique_lock来使用,因为作用域内可能条件不满足需要解锁,lock_guard不支持这种 *** 作
- 常用的成员函数:wait()/notify_one()/notify_all(),详见 条件变量成员函数
- 线程运行到条件后,如果条件不满足,阻塞在条件变量,wait函数内部会解锁,然后等待条件变量被其他线程激活,被激活后会尝试再次加锁,阻塞条件不成立再继续往下执行
- 下面的例子是比较经典的生产者-消费者模型
#include
#include
#include
#include
#include
using namespace std;
std::mutex myMutex; // mutual exclusive lock
std::condition_variable condVar; // condition variable
std::vector<int> vec; // protected object
void Consumer()
{
std::unique_lock<std::mutex> lock(myMutex);
while (vec.empty()) {
condVar.wait(lock);
}
std::cout << "consumer " << vec.size() << "\n";
}
void Producer()
{
std::unique_lock<std::mutex> lock(myMutex);
vec.push_back(1);
condVar.notify_one(); // or condVar.notify_all()
std::cout << "producer \n";
}
int main()
{
std::thread newThread(Consumer);
//newThread.detach(); // main-thread or sub-thread split
Producer();
newThread.join();
return 0;
}
2.3 信号量
- C++11中没有信号量,同样的效果可以通过互斥锁和条件变量来实现,且更安全
- C语言中信号量又称信号灯,分为二元信号灯和多元信号灯,二元信号灯就是互斥锁,所以信号灯其实包含了互斥锁,只不过互斥锁用于资源互斥访问,信号量多用于线程同步。
- C语言中信号量常用的成员函数: sem_init()/sem_wait()【P *** 作-】/sem_post()【V *** 作+】/sem_destroy(),详见 信号量的函数 和 C语言信号量实例
- 此处通过互斥锁和条件变量来实现多元信号量,“吃水果问题”
#include
#include
#include
#include
using namespace std;
class Semaphore
{
public:
Semaphore(int value = 1) : count(value) {}
virtual ~Semaphore() = default;
// P *** 作
void P()
{
unique_lock<mutex> lock(myMutex);
--count;
if (count < 0) //资源已经不足挂起等通知
condVar.wait(lock);
}
// V *** 作
void V()
{
unique_lock<mutex> lock(myMutex);
++count;
if (count <= 0) // 有资源了,有线程挂起的话,通知一下
condVar.notify_one();
}
private:
int count;
mutex myMutex;
condition_variable condVar;
};
// 初始化
Semaphore plate(1), apple(0), orange(0);
void Father()
{
while (true)
{
plate.P();
cout << "往盘中放一个苹果" << endl;
apple.V();
}
}
void Mother()
{
while (true)
{
plate.P();
cout << "往盘中放一个橘子" << endl;
orange.V();
}
}
void Son()
{
while (true)
{
apple.P();
cout << "儿子吃苹果" << endl;
plate.V();
}
}
void Daughter()
{
while (true)
{
orange.P();
cout << "女儿吃橘子" << endl;
plate.V();
}
}
int main()
{
std::thread farther(Father), mother(Mother), son(Son), daughter(Daughter);
farther.join();
mother.join();
son.join();
daughter.join();
return 0;
}
- 上面的这个例子自己理一遍应该就懂为什么PV *** 作count要这么设计了
以下内容待补充总结。
2.4 读写锁- 待补充总结 读写锁
- 待补充总结 自旋锁
-
shared_lock用法:shared_lock用法
-
lock_guard和scoped_lock的区别:lock_guard和scoped_lock区别
-
binary_semaphore的用法:binary_semaphore的用法
参考文章:
互斥锁的用法
std::lock的使用
std::scoped的使用
条件变量的使用
信号量的使用
c++信号量使用
c++线程同步方法
created by shuaixio, 2022.04.30
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)