【Linux】线程中的互斥锁、条件变量、信号量(数据安全问题、生产消费模型、阻塞队列和环形队列的实现)
文章目录
- 1、线程互斥
- 1.1 线程间频繁切换导致的问题
- 1.2 使用互斥锁
- 1.3 互斥锁的原理
- 1.4 线程中的数据安全问题
- 2、线程同步之条件变量
- 2.1 生产消费模型
- 2.2 条件变量概念和调用函数
- 2.3 阻塞队列的实现
- 3、线程同步之信号量
- 3.1 理解信号量
- 3.2 信号量接口
- 3.3 环形队列的实现
- 4、小结
1、线程互斥
1.1 线程间频繁切换导致的问题
下面进行一个"售票"小实验,通过让四个线程并发的对一个全局变量进行tickets--
操作,看最后的结果,好让我们意识到多线程并发的问题。
做法就是多个线程并发的执行同一段代码,同时调度器尽可能频繁发生线程调度和切换。
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;//定义一个全局变量,这个变量可以被所有线程访问
//这个数据越大,出现问题的概率越高
int tickets = 10000000;void* GetTicks(void* args)
{while(true){if(tickets > 0) //1、读取数据到寄存器中 2、进程判断{tickets--; //1、读取数据 2、更改数据 3、写回数据}else{//tickets到0退出break;}//后续可能还有其它动作,假设用了500微秒usleep(500);}
}int main()
{
#define NUM 4vector<pthread_t> threads(NUM);for(int i = 0; i < NUM; ++i){pthread_create(&threads[i], nullptr, GetTicks, nullptr);}for(const auto& tid : threads){pthread_join(tid, nullptr);}cout << "最终剩余:" << tickets << endl;return 0;
}
结果出现减到负数。
值得注意的是,线程的调度顺序是随机的,也就是说每次运行结果都是随机的。
下面探讨就是为什么会出现随机结果
(值得注意的是,共享数据越大,出现问题的概率越高)
那么为什么会发生上面这个现象呢?
首先值得说明的是,tickets–这个操作在汇编中是有三步的。
其次,一个线程在运行一个时间片到后,线程切换调度另一个线程。线程从内核态切换回用户态前,线程要对调度状态进行检测,如果可以就进行切换。(其实就是在调用系统接口前判断状态,然后是内核态就切换)
可能当一个线程执行到tickets对应的代码时,比如进入tickets>0判断并且还未减一操作,这时该线程被切换走了,上下文数据被保存,线程切换换成了下一个线程,而下一个线程减一操作让tickets减为了0;当原来线程被切回来时,读取内存中tickets为0的数据,继续调用还未减一的操作,那么执行完操作后就给内存返回了一个负数。
这种就是典型的由于线程执行顺序不确定导致的竞态条件问题。
通过以上还可以直观的理解一些概念:
- 上述tickets对所有线程来说就可以称为一种共享资源,而如果通过某种手段让多个执行流安全的访问共享资源:此时这个共享资源就成为临界资源 (比如上面的tickets全局变量,但是目前暂时还没有通过一些手段让执行流安全访问它,所以只算是共享资源,假设它已经能被安全访问了,就有以下概念)
- 把多个执行流中,访问临界资源的代码,叫做临界区(很小部分,比如上面线程执行函数中访问tickets的两句代码),而其它部分就是非临界区。
- 对一个资源访问的时候,要么不做,要么做完就是原子性的体现。目前可以理解成一个对资源进行的操作,如果只用一条汇编就能完成,那么这个操作就是原子性的。(可惜上面tickets减1操作由三条汇编完成,不是原子的。)
当然上述代码不仅可能因为tickets>0
出问题,还可能在tickets减1操作上出问题
1.2 使用互斥锁
为了解决上面出现的线程频繁切换导致共享资源数据异常问题,可以通过一个互斥锁来初步解决:
- 当代码执行临界区后,不允许其它线程切换。
- 如果多个线程在执行,并且目前没有线程进入临界区,那么只能让一个线程进入临界区。
- 如果线程不在临界区执行,那么不能阻止其它进入临界区的线程。
形象点来说,就是一个带锁的空间,一次只能一个线程访问,只有持有钥匙的线程才能进入,并且在未归还钥匙前,其它线程都不能进入这个空间,只能在外面等待。
这种多线程串行访问临界资源,就称为互斥。
做到以上三点就构成了一把互斥锁,在Linux中也称为互斥量。
互斥量接口:
init初始化一把锁,destroy销毁一把锁。
其中mutex代表一个锁,如果在局部定义,就需要初始化和销毁;如果在全局定义,就直接赋值成PTHREAD_MUTEX_INITIALIZER
上面两个函数都是成功返回0,失败返回错误码。
加锁和解锁
lock加锁
unlock解锁
trylock,如果有锁就不加并且里面返回错误信息,如果没有就加锁。
下面直接通过简单代码演示其功能
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;int tickets = 10000000;
//定义全局锁的话,不用在局部初始化和销毁,线程共享这个锁
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //保存每个线程的属性
struct ThreadData
{string _name;pthread_mutex_t* _mutex_p;ThreadData(const string& name, pthread_mutex_t* mutex_p):_name(name),_mutex_p(mutex_p){}~ThreadData(){}
};void* GetTicks(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);while(true){//在临界区加锁pthread_mutex_lock(td->_mutex_p);if(tickets > 0) //1、读取数据到寄存器中 2、进程判断{//cout << "当前线程:" << td->_name << endl;tickets--; //1、读取数据 2、更改数据 3、写回数据//退出解锁pthread_mutex_unlock(td->_mutex_p);}else{//退出前解锁pthread_mutex_unlock(td->_mutex_p);break;}//后续可能还有其它动作,假设用了500微秒usleep(500);}
}int main()
{
#define NUM 4pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);vector<pthread_t> threads(NUM);for(int i = 0; i < NUM; ++i){//为每个线程创建一个名称char buffer[64];snprintf(buffer, sizeof buffer, "Thread %d", i+1);ThreadData* td = new ThreadData(buffer, &lock);pthread_create(&threads[i], nullptr, GetTicks, (void*)td);}for(const auto& tid : threads){pthread_join(tid, nullptr);}pthread_mutex_destroy(&lock);cout << "最终剩余:" << tickets << endl;return 0;
}
结果就是还是同一1000w量级下,加锁后怎么运行都是0,这解决了问题
不过有一点可以明显感觉到,程序运行变慢了。
这是因为加锁和解锁的频繁切换。
问题并没有完全解决,因为上面代码还有线程饥饿问题:
void* GetTicks(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);while(true){pthread_mutex_lock(td->_mutex_p);if(tickets > 0){cout << "当前线程:" << td->_name << endl;tickets--;pthread_mutex_unlock(td->_mutex_p);}else{pthread_mutex_unlock(td->_mutex_p);break;}//usleep(1); //休眠1微秒}
}
再为代码添加一个打印线程,会发现在线程调度函数时,一个优先级高的线程会一直抢占临界区。这就使得其它线程一直等待,出现线程饥饿问题。
这也是互斥锁的问题。
不过可以在后面添加一个usleep(1);
让当前线程阻塞,进行线程切换,以保证线程依次运行。
那么如何看待锁呢?
首先可以理解一个线程持有锁进入临界区,在其它线程看来,就是原子的(是否持有锁),因此通过锁构建的原子性安全的访问了共享资源。
其次可以从上面代码中看到,lock变量只在主线程定义了一个,但是却可以让四个线程共享。因此其实锁本身就是一个共享资源,而全局的变量是被锁保护的,那么锁的安全怎么被保护呢?
其实pthread_mutex_lock这个加锁操作具有原子性,因此保证了其安全。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
1.3 互斥锁的原理
先来看这几个问题
当一个线程,申请锁成功,进入临界资源,正在访问临界资源期间,其它线程在做什么呢?
其它线程在执行临界区以外的代码后如果需要访问临界区就会等待。
当一个线程,申请锁成功,进入临界资源,正在访问临界资源期间,是否可以被其它线程切换呢?
是可以的,只不过当其它线程切换后,由于之前的线程在被切换的同时也带走了其上下文,对应的锁也被其带走了。因此当其它线程访问临界区会等待,再次进行线程切换,只有再次切换到拥有锁的那个线程才能访问临界区。
因此在理解上,在其它线程看来,当前线程持有锁的过程是原子的(要么有,要么没有)。
下面这张图就解释了实现互斥锁操作的安全性实现问题。
1.4 线程中的数据安全问题
线程中的数据安全问题有以下几种:
竞态条件:多个线程同时竞争同一资源,导致结果依赖于运行时的随机性。(就是第一套代码的问题)
非原子操作:一个操作被多个线程同时执行,导致其中某些操作未能按照期望的顺序执行。(比如第一套代码的tickets减1操作)
死锁问题:多个线程等待彼此释放锁,导致程序无法继续执行。
举个例子:两个小朋友分别有5毛钱,但只能凑齐1块才能买一个棒棒糖,其中一个小朋友想向另一个小朋友索要他的5毛钱,而另一个小朋友又想要他的5毛钱,这就出现了一个互相等待的状态。
在未来需要使用多把锁的情况,我们持有自己的锁不释放,同时也要对方的锁,这就容易造成死锁问题。
一把锁也是可以造成死锁问题的:一把锁加锁两次!
死锁的四个必要条件
1、互斥:只要用了锁就有互斥。(要破坏这个条件只有不用锁)
2、请求与保持:线程本身拥有锁,不释放,还要其它线程的锁。(要破坏这个条件,可以通过trylock调用,当有锁的时候加锁就会报错)
3、不剥夺:当前线程不能直接拿到向另一个线程申请的锁。(而剥夺了就可以破坏死锁,所以可以通过一个竞争策略,当前线程假设已经申请到A锁,接下来又要申请B锁,B锁被其它线程拿到了,这时可以通过两个线程的优先级,让其中优先级低的主动解锁)
4、环路等待条件:当前线程向另一个线程申请锁,另一个线程也同时向当前线程申请锁。(让一个线程同时申请A,B两把锁,不要先一个申请A和B,另一个右申请B和A。)
所以避免死锁就得破坏上面这些条件。
但是其实能不用锁就不用锁
2、线程同步之条件变量
前面说,互斥锁能够通过限制单一线程访问临界区从而解决数据安全的问题。但是互斥锁仅仅只是限制了一个线程的访问,如果单个线程在一定条件下一直持有锁不释放,这就会频繁占用CPU资源,导致其它线程总是等待资源,造成线程饥饿问题,这非常不合理。(也就是互斥锁限定了一个线程访问临界资源,但是其没有规定哪个线程,这就可能造成一个线程一直抢占的问题,这是对的但不合理)
为了解决这个问题,就需要线程同步,让访问完临界区的线程到后面排队,依次顺序访问临界区。
在访问临界资源安全的前提下,让线程访问某种资源,且具有一定的顺序性(防止饥饿,线程协同),这就是同步。
首先条件变量是一种Linux中最常见的同步机制,它通过一种等待通知机制维持了线程间的同步关系,下面先通过了解一个场景便于更好的理解条件变量。
2.1 生产消费模型
先通过了解生产消费模型能让我们更好的了解条件变量。
在生产消费模型可以通过一个例子理解存在三种个体,分别是生产者、消费者、超市(对应系统中的两个线程和一个缓冲区)。其中顾名思义,就是简单的消费者从超市消费,生产者生产放到超市。
生产消费模型的好处:
消除生产者和消费者的强耦合:超市的出现,使得生产者和消费者之间虽然彼此依赖但又不强加关系。生产者在生产的同时,消费者可以做其它事,而不是等待。(对应在系统上,生产线程在加载到数据到缓冲区的同时,消费线程可以取其它数据)
支持生产和消费忙闲不均问题:生产者和消费者彼此的生产速度和消费速度没有依赖关系,可以随意控制。
提高效率:首先正因为生产和消费可以分离开来,生产者和消费者可以分别以不同的速度和缓存器进行交互(放和拿)。其次,多个生产者在放入缓冲区前可以并行生产数据,多个消费者在拿取后也可以并行消费数据。彼此不影响。(因为生产数据和消费数据可能会用很长时间,因此如果消费数据和生产能并行,就能提高效率)
生产消费模型中个体的彼此关系:
生产者和生产者之间:在生产的过程彼此都是独立不影响的,但是在生产后放在缓冲区的时候是需要互斥的!(其实生活中超市是有很多个货架,所以放货可以一起,但是计算机中数据是会被覆盖的,所以需要互斥!)
消费者和消费者之间:在获取数据后的处理彼此不影响,但是在获取数据的时候,是需要互斥的(不然会出现一个消费者抢另一个消费者的数据)
消费者和生产者之间:首先是需要互斥的,以保证数据安全(因为可能在读取数据的同时,生产者将数据覆盖了)。其次需要一种同步机制,如条件变量。(比如当一种货物没有的时候,如果消费者一直频繁向超市进行询问,那么这会浪费消费者和超市柜员的时间。在计算机中,如果消费线程在加锁访问临界资源时,由于没有资源可以被消费,消费线程就会一直在临界区判断是否有资源可以消费,这就导致其频繁占用CPU资源,造成生产线程的饥饿问题)
那么生产消费模型所需要的同步就很清楚:
1、如果生产线程互斥的将缓冲区资源写满了,那么在缓冲区满的时候,就需要一种通知与等待机制让该线程进入一个等待队列,直到其它线程通知它缓冲区还有空位,就可以再次运行它。
2、反之,如果消费线程将缓冲区资源读完了,那么在缓冲区空的时候,就需要一种通知与等待机制让该线程进入一个等待队列,直到其它线程通知它缓冲区还有数据,就可以再次运行它。
这种通知与等待机制就需要同步机制中的条件变量。
2.2 条件变量概念和调用函数
条件变量的使用,能够让一个线程在满足某些条件后等待,直到其它线程通知它条件已满足。
通过条件变量可以实现等待和通知机制。
条件变量也是pthread库中的一个类型,用关键字cond表示。
条件变量的创建和销毁
cond在创建和销毁上用法和mutex基本一致。
如果在局部需要先创建一个临时变量(比如pthread_cond_t cond
),再通过 init 初始化。
init函数调用cond参数对应创建的临时变量,attr参数访问其属性(不需要修改就置为nullptr)
如果要销毁临时变量,就通过 destroy 调用。
如果在全局创建就可以直接写pthread_cond_t cond = PTHREAD_COND_INITIALIZER
条件变量中等待和唤醒
让当前线程在指定的cond上等待
pthread_cond_wait:
该函数的目的就是当线程在临界区中运行时,如果满足了某种条件,就将当前线程在指定的cond上进行等待。当满足条件后由其它线程通过信号将其唤醒,如果没有会一直等待,直到有信号或超时终止。
函数实现的细节: 等待时自动释放锁,好让其它线程访问共享资源,之后加入一个等待队列,当满足条件后,函数会重新获取锁。
因此:
cond参数需要指定线程在哪一个条件变量上等待。
mutex参数需要指定当前线程被上的锁。
timedwait是指定了等待时间其它都一样,一般用wait控制好就行。
向在cond上等待的线程发送一个信号,使其退出等待队列,正常运行。
其中:
broadcast:可以让所有在cond对应等待队列的线程收到信号。
signal:让其中一个在cond对应等待队列最前的线程收到信号。
理解条件变量
条件变量其实就是一个类型。有着一个自己的等待队列,并且有着多种属性(如状态)。
一个例子:
假设有一个面试现场(临界区),在面试现场的一块区域中有一个面试官(共享资源)正在面试一个面试者(线程),在这个区域外,有很多其它面试者(线程)正在无序地等待面试。无序就造成了当一个面试者面试完成出来后,其它面试者就争着看谁声音大寻求面试,这也就容易造成一些面试者抢不到面试机会(饥饿问题)。因此如果再让一个公司的人(条件变量)在门口建立一个等待队列,让面试的人依次排队,后来的人在队尾,这样就是一个合理有效的方案。
2.3 阻塞队列的实现
下面通过代码,将生产消费模型和条件变量的使用进行融合。
阻塞队列的思路
- 建立一个阻塞队列作为一个缓冲区,当生产者生产时就是入队操作,当消费者消费时就是出队操作。
- 生产的具体过程中,由于需要互斥生产,所以在入队前需要加锁,入队后需要解锁。并且在入队前需要判断队列是否已满,如果满了需要让线程进入等待。如果入队成功说明必不为空可以向空的条件变量线程发送信号。
- 消费的具体过程中,由于需要互斥消费,所以在出队前需要加锁,出队后需要解锁。并且在出队前需要判断队列是否空,如果空了需要让线程进入等待。如果出队成功说明必不为满可以向满的条件变量线程发送信号。
- 生产和消费的关系上,需要维持同步关系,当队列为满时,需要让生产线程进入等待,并且唤醒等待的消费线程,反之队列为空消费线程进入等待,并唤醒等待的生产线程。
- 生产的数据可以通过随机生成几个数字,消费可以通过算几个简单加减法演示。
- 再建立一个阻塞队列,利用其存储处理完的数据,再写入文件。
阻塞队列类的实现
#pragma once#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::queue;//定义阻塞队列最大容量
const int maxcap = 500;template<class T>
class BlockQueue
{
private:queue<T> _q;int _capacity;pthread_cond_t not_full_cond;//没有满的条件pthread_cond_t not_empty_cond;//没有空的条件pthread_mutex_t _mutex;
public:BlockQueue(const int& capacity = maxcap):_capacity(capacity){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(¬_full_cond, nullptr);pthread_cond_init(¬_empty_cond, nullptr);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(¬_full_cond);pthread_cond_destroy(¬_empty_cond);}void push(const T& in) //输入型参数 const T&{//访问共享资源必须是互斥访问,所以加锁pthread_mutex_lock(&_mutex);while(isfull()){//当队列满,就在没有满的条件进行等待。pthread_cond_wait(¬_full_cond, &_mutex);}_q.push(in);//入队成功说明没有空的条件成立,向等待这个条件的线程发送信号pthread_cond_signal(¬_empty_cond);pthread_mutex_unlock(&_mutex);usleep(1);}void pop(T* out) //输出型参数 T* 输入输出型参数T&{pthread_mutex_lock(&_mutex);while(isempty()){pthread_cond_wait(¬_empty_cond, &_mutex);}*out = _q.front();_q.pop();pthread_cond_signal(¬_full_cond);pthread_mutex_unlock(&_mutex);usleep(1);}
private:bool isfull(){return _q.size() == _capacity;}bool isempty(){return _q.empty();}
};
细节一:这里使用while而不是if的原因是,当多个线程收到信号退出等待队列,可能在其中一个线程生产后队列又满了,所以其它线程还需要再判断条件进入等待队列,不然会出现虚假唤醒。
并且,在wait介绍那里也说了,等待时自动释放锁,好让其它线程访问共享资源,之后加入一个等待队列,当满足条件后,函数会重新获取锁。
while(isfull())
{pthread_cond_wait(¬_full_cond, &_mutex);
}
细节二:这两个函数谁在前谁在后都无所谓,因为发信号和该线程无关。
pthread_cond_signal(¬_empty_cond);
pthread_mutex_unlock(&_mutex);
任务类
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<unordered_map>
#include<string.h>//传输任务
class CalTask
{//定义一个函数类型using func_t = std::function<int(int, int, char)>;
public:CalTask(){}CalTask(const int& x, const int& y, const char& op, func_t func):_x(x) ,_y(y) ,_op(op) ,_callback(func){}//消费者返回的一个处理结果std::string operator()(){char buffer[64];snprintf(buffer, sizeof buffer, "%d %c %d=%d", _x, _op, _y, _callback(_x, _y, _op));return buffer;}//生产者生产的一个处理任务std::string toTaskString(){char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;}
private:int _x;int _y;char _op;func_t _callback;
};//一共有以下operation
const std::string oper = "+-*/%";int mymath(int x, int y, char op)
{std::unordered_map<char, std::function<int(int, int)>> dict ={{'+', [](int x, int y){ return x + y; }},{'-', [](int x, int y){ return x - y; }},{'*', [](int x, int y){ return x * y; }},{'/', [](int x, int y){ if(y == 0) {std::cerr << "div zero error!" << std::endl; return -1;} else return x * y; }},{'%', [](int x, int y){ if(y == 0) {std::cerr << "mod zero error!" << std::endl; return -1;} else return x % y; }}};int ret = dict[op](x, y);return ret;
}//存储任务
class SaveTask
{using func_t = std::function<void(const std::string&)>;
public:SaveTask(){}SaveTask(const std::string& message, func_t func):_message(message),_func(func){}void operator()(){_func(_message);}private:std::string _message;func_t _func;
};void Save(const std::string& message)
{FILE* fp = fopen("./log.txt", "a+");if(fp == nullptr){std::cout << strerror(errno) << std::endl;return;}fputs(message.c_str(), fp);fputs("\n", fp);fclose(fp);
}
其中有一个传输任务和一个存储任务。
传输任务就是收到数据后进行简单的加减乘除。
存储任务就是将结果放在一个log.txt文件中
主执行
#include "BlockQueue.hpp"
#include "Task.hpp"//定义一个类,包括传输的队列和存储的队列
template<class C, class S>
class BlockQueues
{
public:BlockQueue<C>* _cal_bq;BlockQueue<S>* _save_bq;
};//生产者
void* productor(void* args)
{//类型安全转换BlockQueues<CalTask, SaveTask>* bq = static_cast<BlockQueues<CalTask, SaveTask>*>(args);while(true){int x = rand()%100;int y = rand()%10;int op_count = rand()%oper.size();CalTask in(x, y, oper[op_count], mymath);bq->_cal_bq->push(in);cout << "生产者:" << in.toTaskString() << endl;}return nullptr;
}void* consumer(void* args)
{BlockQueues<CalTask, SaveTask>* bq = static_cast<BlockQueues<CalTask, SaveTask>*>(args);while(true){CalTask out;bq->_cal_bq->pop(&out);cout << "消费者:" << out() << endl;SaveTask sv(out(), Save);bq->_save_bq->push(sv);cout << "推送储存任务完成....." << endl;//可以控制消费者速度//sleep(1);}return nullptr;
}void* saver(void* args)
{BlockQueue<SaveTask>* bq = (static_cast<BlockQueues<CalTask, SaveTask>*>(args))->_save_bq;while(true){SaveTask t;bq->pop(&t);t();cout << "保存任务完成..." << endl;}return nullptr;
}int main()
{srand((unsigned long)time(nullptr) * 0x123543);BlockQueues<CalTask, SaveTask> bq;bq._cal_bq = new BlockQueue<CalTask>();bq._save_bq = new BlockQueue<SaveTask>();pthread_t p[3], c[2], s;pthread_create(&p[0], nullptr, productor, (void*)&bq);pthread_create(&p[1], nullptr, productor, (void*)&bq);pthread_create(&p[2], nullptr, productor, (void*)&bq);pthread_create(&c[0], nullptr, consumer, (void*)&bq);pthread_create(&c[1], nullptr, consumer, (void*)&bq);pthread_create(&s, nullptr, saver, (void*)&bq);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);pthread_join(p[2], nullptr);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(s, nullptr);delete bq._cal_bq;delete bq._save_bq;return 0;
}
运行结果:
3、线程同步之信号量
3.1 理解信号量
在前面条件变量参与的阻塞队列中,存在一些问题。
1、外部不能确定临界资源的情况,如果想要确认临界资源的情况只能通过先加锁再判断确定队列的情况。
2、多线程在并发访问阻塞队列时,是以一个整体资源去访问,并且是对确定位置的操作(固定的队尾入数据,队头出数据)。如果需要多线程并发访问共享资源的不同区域,这就不太行了。
接下来就需要引入信号量。
首先,信号量可以理解为一个计数器,用来衡量临界资源中资源的多少。
其次,如果在访问临界资源前申请了一个信号量,就可以提前对临界资源的情况进行确认。(就不再需要像阻塞队列中那样先加锁再判断了)
那么所有线程就必须看到信号量,信号量本身就是一个共享资源。
共享资源需要被安全访问,那么信号量是如何保证本身安全的呢?
信号量作为一个计数器,自然就有两种功能:
1、sem减1,对应申请资源,这个操作是一种原子操作,因此保证了线程访问其是安全的。其也简称P操作
2、sem加1,对应归还资源,这个操作是一种原子操作,因此保证了线程访问其是安全的。其也简称V操作
两个PV原子操作语句,也称为PV原语。
信号量一旦申请成功,就意味着能保证申请的线程一定拥有一部分临界资源。(资源预订)
这也就意味着能使多个线程并发的访问到同一共享资源的不同区域。
下面先通过介绍接口,再通过一个结构的实现来体会信号量。
3.2 信号量接口
在Linux中信号量用sem关键字表示。
其调用的接口都是在<semaphore.h>
头文件下。
信号量的初始化和销毁
初始化一个信号量
信号量也是一开始要定义的。
sem参数就是传定义的变量。
pshared:0表示线程间共享,非0表示进程间共享。
value:表示信号量初始值。(这个就设置为其代表共享资源的意义的值,比如设置为共享资源空间一开始有多大)
返回值成功返回0,错误返回错误码
销毁一个信号量,这个很简单,直接传定义的地址。
等待和发表信号量
这两个就是对应PV操作。
等待信号量,会将信号量的值减1,参数传定义的地址。
发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
下面直接通过代码感悟
3.3 环形队列的实现
首先这个环形队列也是基于生产消费模型来实现。
环形队列在数据结构上,本质是一个定长数组,通过两个下标指向入队和出队的位置(front,end),在环形队列为空时,两个下标指向同一位置,在环形队列为满时,两个指针也指向同一位置(这里不考虑空结点实现),两个下标通过取模运算控制范围。
具体实现细节:
1、环形队列通过一个固定大小的数组来存储数据。
2、在生产者访问临界区前,需要通过P操作来判断是否还有剩余空间,如果没有就需要等待。
3、反之消费者访问临界区前,也需要通过V操作来判断是否还有剩余资源,没有就等待。这样就可以维护生产者和消费者之间的同步关系。(不然会出现生产者和消费者对同一区域进行读写)
4、比如多个生产者在访问临界区时,肯定也是需要保持互斥的,那么就需要互斥锁。
5、生产的数据可以通过随机生成几个数字,消费可以通过算几个简单加减法演示。
环形队列类:
//"RingQueue.hpp"
#pragma once#include <iostream>
#include <vector>
#include <cassert>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>const int& mxcap = 500;template<class T>
class RingQueue
{
private://线程要访问临界资源中的某一块区域 申请信号量 所有人都要看到信号量,信号量因此也是个共享资源//访问共享资源要被保护,那么其操作就应该是原子的 PV原语void P(sem_t& sem){//wait使信号量减一int n = sem_wait(&sem);assert(n == 0);(void)n;}void V(sem_t& sem){//post使信号量加一int n = sem_post(&sem);assert(n == 0);(void)n;}
public:RingQueue(const int& cap = mxcap) :_cap(cap), _queue(cap){sem_init(&_spaceSem, 0, _cap);sem_init(&_dataSem, 0, 0);pthread_mutex_init(&_pmutex, nullptr);pthread_mutex_init(&_cmutex, nullptr);_ProductorIdx = _ConsumerIdx = 0;}void Push(const T& in){//生产对应剩余空间减一P(_spaceSem);pthread_mutex_lock(&_pmutex);_queue[_ProductorIdx++] = in;_ProductorIdx = _ProductorIdx % _cap;pthread_mutex_unlock(&_pmutex);V(_dataSem);usleep(1);}void Pop(T* out){//消费对应空间数据减一P(_dataSem);pthread_mutex_lock(&_cmutex);*out = _queue[_ConsumerIdx++];_ConsumerIdx = _ConsumerIdx % _cap;pthread_mutex_unlock(&_cmutex);V(_spaceSem);usleep(1);}~RingQueue(){sem_destroy(&_spaceSem);sem_destroy(&_dataSem);pthread_mutex_destroy(&_pmutex);pthread_mutex_destroy(&_cmutex);}
private:std::vector<T> _queue;int _cap;sem_t _spaceSem; //生产者看到的是剩余的空间sem_t _dataSem; //消费者需要看到的是数据int _ProductorIdx; //用下标确实生产消费者的位置int _ConsumerIdx;pthread_mutex_t _pmutex;pthread_mutex_t _cmutex;
};
任务类:
//"Task.hpp"
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<unordered_map>
#include<string.h>using std::cout;
using std::endl;class CalTask
{using func_t = std::function<int(int, int, char)>;
public:CalTask(){}CalTask(const int& x, const int& y, const char& op, func_t func):_x(x) ,_y(y) ,_op(op) ,_callback(func){}std::string operator()(){char buffer[64];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, _callback(_x, _y, _op));return buffer;}std::string toTaskString(){char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);return buffer;}
private:int _x;int _y;char _op;func_t _callback;
};const std::string oper = "+-*/%";int mymath(int x, int y, char op)
{std::unordered_map<char, std::function<int(int, int)>> dict ={{'+', [](int x, int y){ return x + y; }},{'-', [](int x, int y){ return x - y; }},{'*', [](int x, int y){ return x * y; }},{'/', [](int x, int y){ if(y == 0) {std::cerr << "div zero error!" << std::endl; return -1;} else return x / y; }},{'%', [](int x, int y){ if(y == 0) {std::cerr << "mod zero error!" << std::endl; return -1;} else return x % y; }}};int ret = dict[op](x, y);return ret;
}
主执行:
#include "RingQueue.hpp"
#include "Task.hpp"void* ProductorRoutine(void* rq)
{RingQueue<CalTask>* _rq = static_cast<RingQueue<CalTask>*>(rq);while(true){//生产int x = rand()%10;int y = rand()%20;int op_rand = rand()%oper.size();CalTask ct(x, y, oper[op_rand], mymath);_rq->Push(ct);cout << "生产任务:" << ct.toTaskString() << endl;sleep(1); //控制速度}
}void* ConsumerRoutine(void* rq)
{RingQueue<CalTask>* _rq = static_cast<RingQueue<CalTask>*>(rq);while(true){//消费CalTask ct;_rq->Pop(&ct);cout << "消费任务:" << ct() << endl;sleep(2);}
}int main()
{srand((unsigned long)time(nullptr) ^ 0x12345678);pthread_t p[3], c[6];RingQueue<CalTask>* rq = new RingQueue<CalTask>();for(int i = 0; i < 3; ++i) pthread_create(p+i, nullptr, ProductorRoutine, rq);for(int i = 0; i < 6; ++i) pthread_create(c+i, nullptr, ConsumerRoutine, rq);for(int i = 0; i < 3; ++i) pthread_join(p[i], nullptr);for(int i = 0; i < 6; ++i) pthread_join(c[i], nullptr);delete rq;return 0;
}
运行结果
4、小结
总结一下:
多线程在并发没有安全访问共享资源时是会由于执行顺序不确定出现数据异常问题的,这也是竞态问题。因此就需要互斥锁,以保证其安全访问共享资源。接着互斥锁也是一种共享资源,因此通过了解了它的原理后就明白,互斥锁的实现原理本质也是一种原子操作,这就保证了其安全性。
互斥锁虽然能解决数据安全问题,但是因为其不合理性,引入了线程同步。
线程同步常用的就是条件变量,保证了多线程访问临界资源的顺序性,而通过生产消费模型和阻塞队列就能很好的认识条件变量。
信号量的引入,能让线程提前知道共享资源的情况,并且事先预定资源,再通过程序员自己的实现就可以使得多个线程能并发访问临界资源的不同区域。环形队列的实现也很好的体现了这些。
本章完~
相关文章:

【Linux】线程中的互斥锁、条件变量、信号量(数据安全问题、生产消费模型、阻塞队列和环形队列的实现)
文章目录1、线程互斥1.1 线程间频繁切换导致的问题1.2 使用互斥锁1.3 互斥锁的原理1.4 线程中的数据安全问题2、线程同步之条件变量2.1 生产消费模型2.2 条件变量概念和调用函数2.3 阻塞队列的实现3、线程同步之信号量3.1 理解信号量3.2 信号量接口3.3 环形队列的实现4、小结1、…...

MySQL8.0的安装和配置
🎉🎉🎉点进来你就是我的人了 博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔🦾&am…...

LinuxGUI自动化测试框架搭建(三)-虚拟机安装(Hyper-V或者VMWare)
(三)-虚拟机安装(Hyper-V或者VMWare)1 Hyper-V安装1.1 方法一:直接启用1.2 方法二:下载安装1.3 打开Hyper-V2 VMWare安装注意:Hyper-V或者VMWare只安装一个,只安装一个,只…...

改进YOLO系列:数据增强扩充(有增强图像和标注),包含copypaste、翻转、cutout等八种增强方式
这里写目录标题 一、简介二、数据增强方法介绍复制-粘贴(Copy-paste)翻转(Flip)Cutout加噪声(Noise)亮度调整(Brightness)平移(Shift)旋转(Rotation)裁剪(Crop)copy-paste的代码一、简介 数据增强是一种通过对原始数据进行随机变换、扰动等操作来生成新的训练样…...

c++11 标准模板(STL)(std::stack)(一)
定义于头文件 <stack> template< class T, class Container std::deque<T> > class stack;std::stack 类是容器适配器,它给予程序员栈的功能——特别是 FILO (先进后出)数据结构。 该类模板表现为底层容器的包装…...

C++-c语言词法分析器
一、运行截图 对于 Test.c 的词法分析结果 对于词法分析器本身的源代码的分析结果 二、主要功能 经过不断的修正和测试代码,分析测试结果,该词法分析器主要实现了以下功能: 1. 识别关键字 实验要求:if else while do for main…...

Maven工具复习
Maven从入门到放弃Maven概述Maven 的配置Maven的基本使用IDEA 配置MAVENMaven坐标IDEA 创建MavenIDEA 导入Maven关于右侧Maven小标签(也就是Maven面板)找不到问题的解决办法关于不小心把IDEA主菜单搞消失的解决办法依赖管理Maven概述 Maven是一个工具提供了一套标准的项目结构…...

算法总结-深度优先遍历和广度优先遍历
深度优先遍历(Depth First Search,简称DFS) 与广度优先遍历(Breath First Search,简称BFS)是图论中两种非常重要的算法,生产上广泛用于拓扑排序,寻路(走迷宫),搜索引擎,爬虫等。 一、深度优先遍历 深度优先…...

【Linux】Centos安装mvn命令(maven)
🍁博主简介 🏅云计算领域优质创作者 🏅华为云开发者社区专家博主 🏅阿里云开发者社区专家博主 💊交流社区:运维交流社区 欢迎大家的加入! 文章目录一、下载maven包方法一:官…...

驱动保护 -- 通过PID保护指定进程
一、设计界面 1、添加一个编辑框输入要保护的进程PID,并添加两个按钮,一个保护进程,一个解除保护 2、右击编辑框,添加变量 二、驱动层代码实现 1、声明一个受保护的进程PID数组 static UINT32 受保护的进程PID[256] { 0 }; 2…...

spring常用注解(全)
一、前言 Spring的一个核心功能是IOC,就是将Bean初始化加载到容器中,Bean是如何加载到容器的,可以使用Spring注解方式或者Spring XML配置方式。 Spring注解方式减少了配置文件内容,更加便于管理,并且使用注解可以大大…...

Axios请求(对于ajax的二次封装)——Axios请求的响应结构、默认配置
Axios请求(对于ajax的二次封装)——Axios请求的响应结构、默认配置知识回调(不懂就看这儿!)场景复现核心干货axios请求的响应结构响应格式详解实际请求中的响应格式axios请求的默认配置全局axios默认值(了解…...

(三)【软件设计师】计算机系统—CPU习题联系
文章目录一、2014年上半年第1题二、2014年下半年第3题三、2017年上半年第1题四、2009年下半年第1题五、2010年上半年第5题六、2011年下半年第5题七、2011年下半年第6题八、2012年下半年第1题九、2019年上半年第1题十、2010年上半年第1题十一、2011年上半年第1题十二、2016年下半…...

win下配置pytorch3d
一、配置好的环境:py 3.9 pytorch 1.8.0 cuda 11.1_cudnn 8_0 pytorch3d 0.6.0 CUB 1.11.0 你可能觉得pytorch3d 0.6.0版本有点低,但是折腾不如先配上用了,以后有需要再说。 (后话:py 3.9 pytorch 1.12.1 cuda …...

JS字符串对象
、 JS字符串对象 1.1 内置对象简介 在 JavaScript 中,对象是非常重要的知识点。对象可以分为两种:一种是“自定义对象”外一种是“内置对象”。自定义对象,指的是需要我们自己定义的对象,和“自定义函数”是一些道理;内置对象,…...

Linux系统对文件及目录的权限管理(chmod、chown)
1、身份介绍 在linux系统中,对文件或目录来说访问者的身份有三种: ①、属主用户,拥有者(owner)文件的创建者 ②、属组用户,和文件的owner同组的用户(group); ③、其他用…...

半透明反向代理 (基于策略路由)
定义 半透明反向代理一般是指 代理本身对于客户端透明,对于服务端可见。 从客户端视角看,客户端访问的还是服务端,客户端不知道代理的存在。 从服务端视角看,服务端只能看到代理,看不到真实的客户端。 示意图 客户端…...

课前测5-超级密码
目录 课前测5-超级密码 程序设计 程序分析 课前测5-超级密码 【问题描述】 上次设计的“高级密码”被你们破解了,一丁小朋友很不服气! 现在,他又设计了一套更加复杂的密码,称之为“超级密码”。 说实话,这套所谓的“超级密码”其实也并不难: 对于一个给定的字符…...

QML控件--Menu
文章目录一、控件基本信息二、控件使用三、属性成员四、成员函数一、控件基本信息 二、控件使用 import QtQuick 2.10 import QtQuick.Window 2.10 import QtQuick.Controls 2.3ApplicationWindow{visible: true;width: 1280;height: 720;Button {id: fileButtontext: "Fi…...

002:Mapbox GL更改大气、空间及星星状态
第002个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中更改大气、空间及星星状态 。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共71行)相关API参考:专栏目标示例效果 配置方式 1)查看基础设置:…...

2022年第十三届蓝桥杯题解(全)C/C++
A题就是一个简单的进制转化,代码实现如下: #include <bits/stdc.h>using namespace std;const int N 1e5 10;int main() {int x 2022;int a 1;int res 0;while(x) {res (x % 10) * a;a a * 9;x / 10;}cout << res;return 0; } B题有…...

【cmake学习】find_package 详解
find_package 主要用于查找指定的 package,主要支持两种搜索方法: Config mode:查找 xxx-config.cmake或 xxxConfig.cmake的文件,如OpenCV库的OpenCVConfig.cmakeModule mode:查找Findxxx.cmake文件,如Ope…...

WEB攻防-通用漏洞PHP反序列化POP链构造魔术方法原生类
目录 一、序列化和反序列化 二、为什么会出现反序列化漏洞 三、序列化和反序列化演示 <演示一> <演示二> <演示二> 四、漏洞出现演示 <演示一> <演示二> 四、ctfshow靶场真题实操 <真题一> <真题二> <真题三> &l…...

Baumer工业相机堡盟工业相机如何通过BGAPISDK里的图像处理库进行图像转换(C++)
Baumer工业相机堡盟工业相机如何通过BGAPI SDK进行图像转换(C)Baumer工业相机Baumer工业相机的SDK里图像格式转换的技术背景Baumer工业相机通过BGAPI SDK进行图像转换调用BGAPI SDK的图像转换库ImageProcessor调用BGAPI SDK建立图像调用BGAPI SDK转换图像…...

JD开放平台接口(获得JD商品详情, 按关键字搜索商品,按图搜索京东商品(拍立淘), 获得店铺的所有商品,获取推荐商品列表, 获取购买到的商品订单列表)
参数说明 通用参数说明 url说明 https://api-gw.onebound.cn/平台/API类型/ 平台:淘宝,京东等, API类型:[item_search,item_get,item_search_shop等]version:API版本key:调用key,测试key:test_api_keysecret:调用secret,测试secret:(不用填写…...

上海亚商投顾:沪指震荡反弹 游戏、传媒概念股再度大涨
上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 市场情绪大小指数今日走势分化,沪指向上震荡反弹,创业板指一度跌近1%,黄白二线大幅背离。…...

C/C++ 玩转StoneValley库:从入门到精通
C/C 玩转StoneValley库:从入门到精通引言(Introduction)StoneValley库简介(Overview of StoneValley Library)为什么要学习StoneValley库(Why Learn StoneValley Library in C)StoneValley库安装…...

CentOS7-部署Tomcat并运行Jpress
1. 简述静态网页和动态网页的区别。 2. 简述 Webl.0 和 Web2.0 的区别。 3. 安装tomcat8,配置服务启动脚本,部署jpress应用。1、简述静态网页和动态网页的区别 静态网页: 请求响应信息,发给客户端进行处理,由浏览器进…...

菜鸟程序员的3年心酸逆袭之旅!今天你对我爱搭不理,明天我让你高攀不起!
多年前我以一个菜鸟的身份 进入了一家创业公司 我原本以为公司是这样的 但是实际上是这样的 我进去时 我们部门除开部门老大还有我 也只有我 所以我就这样开始了我的程序员生涯 开始了我的苦逼技术 公司是做电商网站的 因为我是一个菜鸟 所以我接到的第一个任务 就是做一个网页…...

【Scala】异常 隐式转换 泛型
目录 异常 隐式转换 隐式函数 隐式参数 隐式类 隐式解析机制 泛型 泛型上下限 上下文限定 来源: 异常 def main(args: Array[String]): Unit {try {var n 10 / 0}catch {case ex: ArithmeticException>{// 发生算术异常println("发生算术异常&quo…...