【linux多线程】c++多线程的同步方式_ros多线程 同步-程序员宅基地

技术标签: C/C++  c++  linux多线程  线程同步  多线程编程  开发语言  

序言

1. 同步与互斥

  • 互斥和同步的区别:互斥是对共同资源的互斥访问,访问是乱序的,同步就是协调步调,按照一定顺序执行。同步一般已经实现了互斥,通过条件设置实现顺序访问。

  • C语言线程互斥和同步方式,在文章 C语言多线程同步方式一C语言多线程同步方式二中已经做了介绍且有实例。

  • C语言线程同步的常见方式:

    1. 互斥锁
    2. 条件变量
    3. 读写锁
    4. 自旋锁
    5. 信号量
    6. 关卡
    

    以下着重介绍几种常用的C++线程同步方式,包括互斥锁、条件变量、信号量

2. C++多线程同步方式

2.1 互斥锁
  • c++中互斥锁的使用比c复杂一些。c++可使用的互斥锁相关的类有

    1. std::mutex
    2. std::lock
    3. std::lock_guard
    4. std::unique_lock
    5. std::scoped_lock
    
2.1.1 使用实例:std::mutex
#include <thread>
#include <mutex>
#include <string>
#include <iostream>

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 <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>

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 <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

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 读写锁
2.5 自旋锁
2.6 关卡

 


参考文章:
互斥锁的用法
std::lock的使用
std::scoped的使用
条件变量的使用
信号量的使用
c++信号量使用
c++线程同步方法

created by shuaixio, 2022.04.30

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/baidu_35692628/article/details/124518306

智能推荐

HDU3567(Eight II)_hdu 3567-程序员宅基地

文章浏览阅读170次。Eight II题目传送门Problem DescriptionEight-puzzle, which is also called “Nine grids”, comes from an old game.In this game, you are given a 3 by 3 board and 8 tiles. The tiles are numbered from 1 to 8 and each covers a grid. As you see, there is a blank grid_hdu 3567

RuntimeError:input must have 3 dimensions, got 4_runtimeerror: input must have 3 dimensions, got 4-程序员宅基地

文章浏览阅读9.7k次,点赞2次,收藏7次。RuntimeError:input must have 3 dimensions, got 4在pytorch环境下使用RNN对CIFAR10数据集进行分析时出现了这个报错解决方法在pytorch环境下使用RNN对CIFAR10数据集进行分析时出现了这个报错描述是需要输入一个3维的向量,但是这里提供了一个4维的解决方法使用inputs = inputs.view(-1,32,32*3),..._runtimeerror: input must have 3 dimensions, got 4

php保留小数位,和四舍五入(sprintf, number_format, round_sprintf("%.2f", $num);-程序员宅基地

文章浏览阅读6.3k次。1sprintfsprintf("%.2f", $num)直接生成两位小数,自带四舍五入2,number_format($num, 2) 生成两位小数,不四舍五入3round($num, 2) 四舍五入为2为小数_sprintf("%.2f", $num);

Python实现最短路问题常见求解算法2——Label Correcting Algorithm(deque)_python label correcting algorithm-程序员宅基地

文章浏览阅读311次。原文链接1. 适用场景可识别网络中存在的负环起点与终点间至少存在一条可行路径可求解单一起点到多点的最短路径2. 负环识别方法在介绍网络负环识别方法之前,先定义几个符号:nnn :网络节点数量CCC :网络弧费用绝对值的最大值根据《netwrok flow》文献介绍,针对label correcting algorithms有以下识别方法:如果某一节点的距离标签小于 −nC−nC−nC 则说明存在负环(在最坏情况下源点到此点经过所有节点,所有的弧费用值的绝对值都是C,则最小的cos_python label correcting algorithm

安卓数据存储的5种方式_安卓数据储存主要包含哪五种方式-程序员宅基地

文章浏览阅读5.5k次。SharedPreferences:①使用键值对的方式进行数据存储(以xml文件形式存储到手机中)②常用与保存用户设置,如CheckBox选择状态、配置信息等注:①commit发生在UI主线程,apply发生在工作线程,线程可能不安全②key和value不应太大文件存储:直接将所需要保存的内容写到手机文件中手机内部、外部存储选择:①数据私有->内部存储②数据不大+访问频繁->内部存储③数据较大->外部存储④数据随应用卸载而删除->storage/sdcard/_安卓数据储存主要包含哪五种方式

JDK1.7配置-程序员宅基地

文章浏览阅读260次。配置JDK环境变量我的电脑鼠标右键→属性→点击“高级”选项卡点击“环境变量”查看是否已存在变量“JAVA_HOME”没有则点击“新建”进行新建变量变量名:JAVA_HOME变量值:C:\Program Files\Java\jdk1.7.0_71(JDK的实际安装路径)查看是否已存在变量“CLASSPATH”没有则点击“新建”进行新建变量变量名:CLASSPATH变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jare)找到变量“_jdk1.7配置

随便推点

python读取二进制文件中的浮点数数组_fin.read() 浮点数-程序员宅基地

文章浏览阅读5.4k次。python读取二进制文件中的浮点数数组_fin.read() 浮点数

Python使用handless时需要校验操作系统!!!_python handless-程序员宅基地

文章浏览阅读555次。import timefrom selenium.webdriver.common.action_chains import ActionChainsfrom selenium.webdriver.common.keys import Keysfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilit..._python handless

UVa 445 - Marvelous Mazes-程序员宅基地

文章浏览阅读830次。题目:给出一串字符输出对应编码。 如果是数字就计数,连续的数字加和,然后输出对应个数的后面的字母。如果是'!'或者换行就换行; 如果是'b'就输出空格,如果是'*'和字母就直接输出。分析:简单题。字符串处理。#include #include #include using namespace std;void output_uva 445

片式磁珠选购常识_片式磁珠选型有什么-程序员宅基地

文章浏览阅读1.1k次。 片式磁珠选购常识什么是磁珠?使用片式磁珠的好处: 磁珠的工作原理是什么?贴片磁珠和贴片电感的区别?片式磁珠和片式电感的应用场合如何正确选择磁珠?如何选用磁珠?如何选择磁珠厂家和经销商? 什么是磁珠?磁珠有很高的电阻率和磁导率,他等效于电阻和电感串联,但电阻值和电感值都随频率变化_片式磁珠选型有什么

java 十进制转十六进制_java十进制转十六进制-程序员宅基地

文章浏览阅读1.1w次,点赞5次,收藏29次。java 十进制转十六进制第一种方法:Integer.toHexString(a)这种方法在蓝桥杯练习系统中会判错,但是测试用例输出的确实正确的。输入 15 输出 F。但是会被判错import java.util.*;class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int a = sc.nextInt(); System.out.println(Integ_java十进制转十六进制

AI视频文案生成器:工作效率翻倍,宣传成果大增-程序员宅基地

文章浏览阅读408次,点赞8次,收藏7次。作为市场营销经理,我最近幸运地了解到一款实用且富有创意的工具—人工智能视频制作。借助此神器,我的文案写作效率大幅提升,同时也收获了更佳的宣传成果。在此,诚邀您与我一同共享这个神奇工具有助于提高我们工作技能方面的发现和体验。1.了解ai视频文案生成器的功能和特点深入了解后

推荐文章

热门文章

相关标签