信号量——Linux并发之魂
引言
今天,我们继续学习Linux线程本分,在Linux条件变量中,我们对条件变量的做了详细的说明,今天我们要利用条件变量来引出我们的另一个话题——信号量内容的学习。
1.复习条件变量
在上一期博客中,我们没有对条件变量做具体的使用,所以,这里我们通过一份代码来复习一下,接下来,我们实现基于BlockingQueue的生产者消费者模型。
1.1何为基于BlockingQueue的生产者消费者模型
BlockingQueue在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
如图:
1.2分析该模型
这里我想写多个生产线程和多个消费线程的模型
我们来分析一下。
- 首先生产任务的过程和消费任务的过程必须是互斥关系,不可以同时访问该队列(此时,这个队列是共享资源)。
- 当队列满时,生产线程就不能再生产任务,必须在特定的条件变量下等待;同理当队列为空时,消费线程就不能再消费任务,也必须在特定的条件变量下等待。
所以,类应这样设计:
template<class T>
class BlockQueue
{
public:BlockQueue(const int &maxcap=gmaxcap):_maxcap(maxcap){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_pcond,nullptr);pthread_cond_init(&_ccond,nullptr);}void push(const T&in)//输入型参数,const &{pthread_mutex_lock(&_mutex);while(is_full()){pthread_cond_wait(&_pcond,&_mutex);}_q.push(in);pthread_cond_signal(&_ccond);pthread_mutex_unlock(&_mutex);}void pop(T*out){pthread_mutex_lock(&_mutex);while(is_empty()){pthread_cond_wait(&_ccond,&_mutex);}*out=_q.front();_q.pop();pthread_cond_signal(&_pcond);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_ccond);pthread_cond_destroy(&_pcond);}
private:bool is_empty(){return _q.empty();}bool is_full(){return _q.size()==_maxcap;}
private:std::queue<T> _q;int _maxcap; //队列中元素的上线pthread_mutex_t _mutex;pthread_cond_t _pcond; //生产者对应的条件变量pthread_cond_t _ccond;
};
由于我们不知道存储的数据类型,所以这里我们选择使用泛型编程的方式。
接下来就是要生产任务,为了可以观察到整个生产和消费任务的过程,我们可以生成两个随机数,然后进行运算。代码如下:
class CalTask
{using func_t = function<int(int, int, char)>;public:CalTask() {}CalTask(int x, int y, char op, func_t func) :_x(x),_y(y),_op(op),_callback(func){}string operator()(){int result=_callback(_x,_y,_op);char buffer[1024];snprintf(buffer,sizeof buffer,"%d %c %d=%d",_x,_op,_y,result);return buffer;}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 char*oper="+-*/%";
int mymath(int x,int y,char op)
{int result=0;switch(op){case '+':result=x+y;break;case '-':result=x-y;break;case '*':result=x*y;break;case '/':if(y==0){cerr<<"div zero error"<<endl;result=-1;}else{result=x/y;}break;case '%':if(y==0){cerr<<"mod zero error"<<endl;result=-1;}else{result=x%y;}default:break;}return result;
}
接下来,我们来写整体的代码。
1.3完整代码
我们要创建三个文件:BlockQueue.hpp Task.hpp Main.cc各文件内容如下所示:
BlockQueue.hpp
#pragma once
#include<iostream>
#include<pthread.h>
#include<cstring>
#include<unistd.h>
#include<cassert>
#include<queue>
using namespace std;
const int gmaxcap=100;
template<class T>
class BlockQueue
{
public:BlockQueue(const int &maxcap=gmaxcap):_maxcap(maxcap){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_pcond,nullptr);pthread_cond_init(&_ccond,nullptr);}void push(const T&in)//输入型参数,const &{pthread_mutex_lock(&_mutex);while(is_full()){pthread_cond_wait(&_pcond,&_mutex);}_q.push(in);pthread_cond_signal(&_ccond);pthread_mutex_unlock(&_mutex);}void pop(T*out){pthread_mutex_lock(&_mutex);while(is_empty()){pthread_cond_wait(&_ccond,&_mutex);}*out=_q.front();_q.pop();pthread_cond_signal(&_pcond);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_ccond);pthread_cond_destroy(&_pcond);}
private:bool is_empty(){return _q.empty();}bool is_full(){return _q.size()==_maxcap;}
private:std::queue<T> _q;int _maxcap; //队列中元素的上线pthread_mutex_t _mutex;pthread_cond_t _pcond; //生产者对应的条件变量pthread_cond_t _ccond;
};
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include<string>
#include <functional>
using namespace std;
class CalTask
{using func_t = function<int(int, int, char)>;public:CalTask() {}CalTask(int x, int y, char op, func_t func) :_x(x),_y(y),_op(op),_callback(func){}string operator()(){int result=_callback(_x,_y,_op);char buffer[1024];snprintf(buffer,sizeof buffer,"%d %c %d=%d",_x,_op,_y,result);return buffer;}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 char*oper="+-*/%";
int mymath(int x,int y,char op)
{int result=0;switch(op){case '+':result=x+y;break;case '-':result=x-y;break;case '*':result=x*y;break;case '/':if(y==0){cerr<<"div zero error"<<endl;result=-1;}elseresult=x/y;}break;case '%':if(y==0){cerr<<"mod zero error"<<endl;result=-1;}else{result=x%y;}default:break;}return result;
}
Main.cc
include "BlockQueue.hpp"
#include "Task.hpp"
#include<sys/types.h>
#include<unistd.h>
#include<ctime>
#include<iostream>
using namespace std;void *productor(void *bqs_)
{BlockQueue<CalTask> *bqs=static_cast<BlockQueue<CalTask>*>(bqs_);while(true){int x=rand()%10+1;int y=rand()%5+1;int opercode=rand()%(sizeof(oper));CalTask T(x,y,oper[opercode],mymath);bqs->push(T);cout<<"生产任务: ";cout<<T.toTaskstring()<<endl;sleep(1);}
}
void *consumer(void *bqs_)
{BlockQueue<CalTask>*bqs=static_cast<BlockQueue<CalTask>*>(bqs_);while(true){CalTask T;bqs->pop(&T);cout<<"消费任务: ";cout<<T()<<endl;}
}
int main()
{BlockQueue<CalTask> bqs;pthread_t p[5];pthread_t c[5];for(int i=0;i<5;i++){pthread_create(&p[i],nullptr,productor,&bqs);pthread_create(&c[i],nullptr,consumer,&bqs);}for(int i=0;i<5;i++){pthread_join(p[i],nullptr);pthread_join(c[i],nullptr);}
}
在代码中,有几个点需要注意一下:
第一点:
pthread_cond_wait的第二个参数一定是我们正在使用的互斥锁,这个函数在被运行时,会以原子性的方式将锁释放,然后将自己挂起,等待被条件变量唤醒。该函数在被唤醒时,会自动重新获取持有的锁,然后继续向下执行。
假如数个生产者线程一起被唤醒,然后先后持有锁,接着继续生产任务,当队列剩余的空间小于这些生产者生产的任务时,就会出现问题,所以让所有被唤醒的线程先通过while循环,如果有剩余的空间,再进行任务的生产活动。
生产线程这样处理,消费线程也要这样处理
大家可以在自己试这敲一下,有问题可以在评论区和我交流。
接下来,我们来查找一下这些代码有哪些"不足的地方"
2.代码中的“不足”
一个线程在操作临界资源时,临界资源必须是满足条件的,然后线程才能对临界资源进行操作。比如:在如上代码中,生产者线程只有在队列(临界资源)有剩余空间的条件下,才能进行下一步操作。
可是,临界资源是否满足生产和消费的条件,我们不能事前得知,只等进入临界资源后,再进行进一步的检测。
所以,一般访问临界资源的过程为:先加锁,再检测,如果条件满足,就进行下一步的操作;反之,就将该线程挂起,释放锁,然后挂起等待,等到条件满足时,重新获得锁,接着进行下一步操作。
因为不可能事先得知是否满足条件,所以我们只能先加锁,进入临界资源内部进行检测。
只要我们申请了信号量,就默认对这部分资源的整体使用,但通常情况下,我们使用的仅仅是临界资源的一小部分。
实际情况中,有没有可能不同的线程访问临界资源不同部分的情况,有可能。所以,前辈大佬们给出了一种解决方案——信号量。
3.信号量
3.1什么是信号量
信号量的本质是一把计数器,一把衡量临界资源多少的计数器。只要拥有信号量,就在未来一定能够拥有临界资源的一部分。
申请信号量的本质:就是对临界资源的预定机制。
比如:我想去看电影,首先我要买票。我一旦买到票,无论我去不去看电影,都会有一个位置属于我。买票的过程==申请信号信号量的过程。
所以,在访问临界资源之前,我们可以申请信号量。通过申请信号量,我们就可以获知临界资源的使用情况。①只要申请成功,就一定有我可以访问的资源。②只要申请失败,说明条件不就绪,只能等待。如此,就不需要进入临界资源再进行检测了。
3.2信号量的相关接口
如上这些借口如果调用成功的话,返回0;调用失败的话,返回-1,并且错误原因被设置。
我们知道信号量的本质是一把计数器,所以信号量必须可以进行递增和递减的操作。
- 信号量-1:申请资源,其过程必须是原子性的。简称P操作。
- 信号量+1:归还资源,其过程必须是原子性的。简称V操作。
所以,信号量的核心操作:PV原语。
接下来,我们就使用信号量来完成我们的基于环形队列的生产消费模型。
3.3用信号量来实现基于环形队列的生产消费模型
3.3.1对环形队列的简单介绍
相信大家在C++学习期间到都模拟实现过环形队列队列。如图:
环形队列的逻辑结构为环形,但其存储结构实际上就是队列,其实就是一个数组,只不过用下标不断的%上队列的长度。
大家在模拟实现环形队列时,大家必定遇到的问题是:当rear==front时,究竟是环形队列已满还是环形队列为空呢?其实,这个问题有多种处理方式,今天就不讲了。
今天,我们的基于环形队列的生产消费模型必须遵守哪些规则呢?
我们来讲一个故事:
张三和李四在一个房间里做游戏,这个房间里有一张大圆桌,桌子上有很多的盘子。规定张三往每个盘子里放一个桃子🍑,然后李四在后边吃桃子🍑,由于李四还要吃桃子,所以速度一定比张三放的速度满。
总结一下,我们发现张三和李四必须满足这些规律:
- 李四不可以超过张三——消费者不可以超过生产者。
- 张三不可以把李四套一个圈——生产者不可以把消费者套一个圈。
- 张三和李四什么时候在一起?①盘子全为空,张三和李四在一起,张三先运行(生产者先运行)。②盘子全为满,张三和李四在一起,李四先运行(消费者先运行)。③其他情况,张三和李四指向不同的位置。
我们将这些规则迁移到环形队列的生产消费模型,就是生产消费模型应该遵守的规则:
①消费者不能超过生产者。②生产者不能把消费者套一个圈。③生产者和消费者什么情况下会在一起呢?空的时候和满的时候,对应不同的处理方式。④只要生产者和消费者指向不同的位置,就可以实现生产者和消费者的并发执行。只有在为空和为 满时,才会出现同步和互斥问题。
那这些规则由什么来保证呢?信号量。信号量是表征临界资源中资源数目的。
1.对于生产者而言,看中的是队列中的剩余空间——空间资源定义一个信号量。
2.对于消费者而言,看中的是队列中的数据——数据资源定义一个信号量。
接下来,我们基于这份伪代码来理解一下,看看能否满足我们的规则。
生产者关注的是队列里的剩余空间,在队列为空时剩余空间为10,所以生产者可以顺利申请到信号量。但是由于空间中这部分资源已经被占用,所以无法归还。但是消费者所关注的队列中的数据资源不知不觉中已经多了一份。所以对消费者信号量应进行V操作。
消费者关注的是队列中的数据资源,队列刚开始为空时,数据资源为0,消费者申请失败。等到生产者申请神域空间成功后,生产了数据。所以消费者可以成功申请到数据资源信号量,然后消费数据。但不知不觉,队列中的剩余空间多了一份,所以应对剩余空间资源的信号量进行V操作。
若队列满时,剩余空间信号量为0,生产者申请信号量失败。此时,数据资源信号量为满,消费者可以申请到信号量,从而进行操作。所以必须消费者先运行。
若队列空时,数据资源信号量为0,消费者申请信号量失败。此时,剩余空间信号量为满,生产者可以申请到信号量,从而进行操作。所以必须生产者先运行。
所以,这伪代码完全符合我们的规则。接下来,我们编写单生产进程和单消费进程的代码。
编写代码
我们创建三个源文件:RingQueue.hpp main.cc Task.hpp
Ringqueue.hpp:
#pragma once#include <iostream>
#include <vector>
#include <cassert>
#include <semaphore.h>
#include <pthread.h>static const int gcap = 5;template<class T>
class RingQueue
{
private:void P(sem_t &sem){int n = sem_wait(&sem);assert(n == 0); // if(void)n;}void V(sem_t &sem){int n = sem_post(&sem);assert(n == 0);(void)n;}
public:RingQueue(const int &cap = gcap): _queue(cap), _cap(cap){int n = sem_init(&_spaceSem, 0, _cap);assert(n == 0);n = sem_init(&_dataSem, 0, 0);assert(n == 0);_productorStep = _consumerStep = 0;pthread_mutex_init(&_pmutex, nullptr);pthread_mutex_init(&_cmutex, nullptr);}// 生产者void Push(const T &in){// ?: 这个代码 有没有优化的可能// 你认为:现加锁,后申请信号量,还是现申请信号量,在加锁?P(_spaceSem); // 申请到了空间信号量,意味着,我一定能进行正常的生产pthread_mutex_lock(&_pmutex); _queue[_productorStep++] = in;_productorStep %= _cap;pthread_mutex_unlock(&_pmutex);V(_dataSem);}// 消费者void Pop(T *out){// 你认为:现加锁,后申请信号量,还是现申请信号量,在加锁?P(_dataSem);pthread_mutex_lock(&_cmutex);*out = _queue[_consumerStep++];_consumerStep %= _cap;pthread_mutex_unlock(&_cmutex);V(_spaceSem);}~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 _productorStep;int _consumerStep;pthread_mutex_t _pmutex;pthread_mutex_t _cmutex;
};
Task.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <functional>class Task
{using func_t = std::function<int(int,int,char)>;// typedef std::function<int(int,int)> func_t;
public:Task(){}Task(int x, int y, char op, func_t func):_x(x), _y(y), _op(op), _callback(func){}std::string operator()(){int result = _callback(_x, _y, _op);char buffer[1024];snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);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)
{int result = 0;switch (op){case '+':result = x + y;break;case '-':result = x - y;break;case '*':result = x * y;break;case '/':{if (y == 0){std::cerr << "div zero error!" << std::endl;result = -1;}elseresult = x / y;}break;case '%':{if (y == 0){std::cerr << "mod zero error!" << std::endl;result = -1;}elseresult = x % y;}break;default:// do nothingbreak;}return result;
}
main.cc
#include "RingQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>std::string SelfName()
{char name[128];snprintf(name, sizeof(name), "thread[0x%x]", pthread_self());return name;
}void *ProductorRoutine(void *rq)
{// RingQueue<int> *ringqueue = static_cast<RingQueue<int> *>(rq);RingQueue<Task> *ringqueue = static_cast<RingQueue<Task> *>(rq);while(true){// version1// int data = rand() % 10 + 1;// ringqueue->Push(data);// std::cout << "生产完成,生产的数据是:" << data << std::endl;// version2// 构建or获取任务 --- 这个是要花时间的!int x = rand() % 10;int y = rand() % 5;char op = oper[rand()%oper.size()];Task t(x, y, op, mymath);// 生产任务ringqueue->Push(t);// 输出提示std::cout << SelfName() << ", 生产者派发了一个任务: " << t.toTaskString() << std::endl;// sleep(1);}
}void *ConsumerRoutine(void *rq)
{// RingQueue<int> *ringqueue = static_cast<RingQueue<int> *>(rq);RingQueue<Task> *ringqueue = static_cast<RingQueue<Task> *>(rq);while(true){//version1// int data;// ringqueue->Pop(&data);// std::cout << "消费完成,消费的数据是:" << data << std::endl;// sleep(1);// version2Task t;//消费任务ringqueue->Pop(&t);std::string result = t(); // 消费也是要花时间的!std::cout << SelfName() << ", 消费者消费了一个任务: " << result << std::endl;// sleep(1);}
}int main()
{srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self() ^ 0x71727374);// RingQueue<int> *rq = new RingQueue<int>();RingQueue<Task> *rq = new RingQueue<Task>();// 单生产,单消费,多生产,多消费 --> 只要保证,最终进入临界区的是一个生产,一个消费就行!// 多生产,多消费的意义??pthread_t p[4], c[8];for(int i = 0; i < 4; i++) pthread_create(p+i, nullptr, ProductorRoutine, rq);for(int i = 0; i < 8; i++) pthread_create(c+i, nullptr, ConsumerRoutine, rq);for(int i = 0; i < 4; i++) pthread_join(p[i], nullptr);for(int i = 0; i < 8; i++) pthread_join(c[i], nullptr);delete rq;return 0;
}
大家可以自己敲一敲,试一下。
写到这里,这篇博客就结束了,下篇博客我们再见。
相关文章:

信号量——Linux并发之魂
欢迎来到 破晓的历程的 博客 引言 今天,我们继续学习Linux线程本分,在Linux条件变量中,我们对条件变量的做了详细的说明,今天我们要利用条件变量来引出我们的另一个话题——信号量内容的学习。 1.复习条件变量 在上一期博客中&…...
自动驾驶中的逆透视变换(Inverse Perspective Mapping,IPM)详解
前言 IPM(Inverse Perspective Mapping,逆透视变换)图的历史可以追溯到计算机视觉和图像处理领域的发展。逆透视变换是一种用于消除图像中透视效应的技术,使得原本由于透视产生的形变得以纠正,进而更准确地描述和理解图像中的场景。比如在行车中的车道线检测,泊车中的常见…...

Python地震波逆问题解构算法复杂信号分析
🎯要点 🎯时域、时频域以及时间和频率相关联偏振特性分析三种算法 | 🎯时域波参数估计算法 | 🎯机器学习模型波形指纹分析算法 | 🎯色散曲线和频率相关波分析算法 | 🎯动态倾斜校正算法 | 🎯声…...

C语言 -- 深入理解指针(二)
C语言 -- 深入理解指针(二) 1. 数组名的理解2. 使用指针访问数组3. 一维数组传参的本质4. 冒泡排序5. 二级指针6. 指针数组7. 指针数组模拟二维数组8. 字符指针变量9. 数组指针变量2.1数组指针变量是什么?2.2 数组指针变量怎么初始化 10. 二维…...
HTTP协议详解
HTTP协议详解 一、HTTP协议概述二、网络基础与HTTP2.1 TCP/IP协议2.2 发送HTTP请求过程2.3 HTTP请求的组成部分 三、HTTP报文HTTP请求报文HTTP响应报文 结语 一、HTTP协议概述 HTTP,即超文本传输协议(Hypertext Transfer Protocol)ÿ…...
一年时间业绩增长2倍,茅台保健酒业公司在川销售的“三板斧”
执笔 | 尼 奥 编辑 | 扬 灵 作为土地面积全国第5、人口总数全国第3、GDP全国第6的产酒、销酒大省,四川酒类消费总额已达800亿元,其中白酒市场规模达到500亿元。 近年来,随着省外名酒提升对四川市场重视,其市场份额也从20年前的3%…...

土豆炒肉做法
菜单:土豆、葱、铁辣子、纯瘦肉、淀粉、生抽、酱油、刀、案板、十三香、盐巴、擦板 流程: 洗土豆,削皮,擦成条,用凉水过滤两遍淀粉,顺便放个燥里洗肉,切成条,按照生抽、酱油、淀粉、…...
VPS拨号服务器:独享的高效与安全
在当今互联网高速发展的时代,虚拟私人服务器(VPS)已成为许多企业和个人用户托管网站、应用程序的首选。特别是带有拨号功能的VPS服务器,以其独特的优势受到广泛关注。本文将深入探讨VPS拨号服务器的独享特性,以及它如何…...

网络安全设备——防火墙
网络安全设备防火墙是一种用来加强网络之间访问控制的特殊网络互联设备。以下是对防火墙的详细解释: 一、定义与基本概念 定义:防火墙是指设置在不同网络(如可信任的企业内部网和不可信的公共网)或网络安全域之间的一系列部件的…...
Redis 管道技术
Redis 管道技术 引言 Redis,作为一个高性能的键值存储系统,被广泛应用于各种场景,如缓存、消息队列等。为了进一步提高Redis的处理能力和效率,Redis管道技术应运而生。本文将深入探讨Redis管道技术的原理、应用及其优势。 什么是Redis管道技术 Redis管道技术是一种允许…...

使用vue3-treeselect问题
1.当vue3-treeselect是单选时,使用watch监听绑定value,无法监听到值清空 对照后将:value改为v-model,如图 2.使用vue3-treeselect全部清空按钮如何置空select的值,使用watch监听 多选:pageInfo.officeName(val) {// …...

每日直播分享车载知识:硬件在环、UDS诊断、OTA升级、TBOX测试、CANoe、ECU刷写、CAN一致性测试:物理层、数据链路层等
每日直播时间:(进腾讯会议方式:QazWsxEdc_2010) 周一到周五:20:00-23:00(讲一个小时,实操两个小时) 周六与周日:9:00-17:0…...

flex布局---子元素未设置高度,默认与父元素同高---侧轴方向的拉伸
父元素未设置固定高度,由子元素高度撑开,并给父元素开启 flex 布局,成为伸缩容器。 父元素中有三个子元素,为伸缩项目,三个伸缩项目都未指定高度,div.inner1 的高度由内容撑开,div.inner2 和 d…...

资源分享—2021版三调符号库
汇总整理平台软件支持过程中客户项目提供的各类资源(包括但不限于符号库、地图模板等),在客户允许情况下进行集团内分享。 本次分享新版国土空间规划【三调符号库(2021版)】,提供SuperMap格式符号库下载。 …...
解决selenium手动下载驱动问题
解决selenium手动下载驱动问题 每次都需要手动下载驱动很头疼,今天发现一个可以自动下载最新驱动的包webdriver_manager,挺不错的 安装依赖包 pip install selenium pip install webdriver_manager from selenium import webdriver from selenium.webdr…...
使用fifo IP核,给fifo写数据,当检测到ALMOST_EMPTY时,为什么不能立即赋值
这涉及到FIFO(先入先出缓冲器)的内部工作机制和时序考虑。让我详细解释为什么在检测到ALMOST_EMPTY信号时不能立即向FIFO写入数据。 ALMOST_EMPTY信号的特性: ALMOST_EMPTY是一个预警信号,表示FIFO中的数据量已经接近空。这个信号…...
【Python123题库】#汽车迷 #编写函数输出自除数 #身份证号基本信息
禁止转载,原文:https://blog.csdn.net/qq_45801887/article/details/140080109 参考教程:B站视频讲解——https://space.bilibili.com/3546616042621301 有帮助麻烦点个赞 ~ ~ Python123题库 汽车迷编写函数输出自除数身份证号基本信息 汽车…...
普通人怎么利用GPT赚钱之SEO优化内容
如何利用GPT撰写SEO优化内容:全面指南 在当今的数字化世界,搜索引擎优化(SEO)是提升网站流量和曝光率的关键。高质量的SEO优化内容不仅可以提高搜索引擎排名,还能吸引更多潜在客户。GPT(生成预训练变换器)作为一种先进的人工智能工具,可以大大提升SEO内容创作的效率和…...

LeetCode热题100刷题8:54. 螺旋矩阵、73. 矩阵置零、48. 旋转图像
54. 螺旋矩阵 class Solution { public:vector<int> spiralOrder(vector<vector<int>>& matrix) {vector<int> vec;if(matrix.empty())return vec;int left0;int right matrix[0].size()-1;int up0;int down matrix.size()-1;while(true) {for(i…...

景联文科技打造高质量图文推理问答数据集,赋能大语言模型提升推理能力
大语言模型在处理推理任务时,不同于人类能够反思错误并修正思维路径,当它遇到自身知识盲区时,缺乏自我校正机制,往往导致输出结果不仅无法改善,反而可能变得更不准确。 需要依赖外部的知识库和推理能力来克服其在理解和…...

深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...