【Linux】线程同步和互斥

目录

  • 一、线程互斥
    • 1.相关概念
    • 2.互斥锁(mutex)
    • 3.互斥锁的原理
    • 4.自定义封装一个锁
  • 二、可重入和线程安全
  • 三、死锁
    • 死锁概念
    • 死锁四个必要条件
    • 如何避免死锁
  • 四、线程同步
    • 1.条件变量
      • 概念
      • 条件变量接口
      • 基于阻塞队列实现生产者消费者模型
    • 2.信号量
      • 概念
      • 信号量操作接口
      • 基于环形队列的生产者消费者模型
  • 五、总结

一、线程互斥

1.相关概念

1.临界资源:多线程执行流共享的资源,且一次只能允许一个执行流访问的资源就叫做临界资源。(多线程、多进程打印数据)

2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

3.互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

4.原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么不执行 。

实现一个小实例

#include<iostream>
#include<unistd.h>
#include<stdio.h>using namespace std; int ticket=10000;void* threadRoutinue(void* args) {const char* name=static_cast<const char*>(args);while(true){if(ticket>0){usleep(1000);//模拟抢票花费时间cout<<name<<"get a ticket: "<<ticket--<<endl;}else{break;}}return nullptr; }int main() {//创建线程模拟抢票pthread_t tid[4];int n=sizeof(tid)/sizeof(tid[0]);for(int i=0;i<n;i++){char buffer[64];snprintf(buffer,sizeof(buffer),"thread_%d",i);pthread_create(tid+i,nullptr,threadRoutinue,buffer);}for(int i=0;i<n;i++){pthread_join(tid[i],nullptr);}return 0; } 

在这里插入图片描述

从程序中可以看到,票数到0的时候就没有票了,线程就应该退出了。

但是结果中,票数甚至被抢到了负数,这是怎么回事。

这里提一个问题,这里对票(临界资源)的访问是原子的吗?(是安全的吗?) 答案肯定不是!!
在这里插入图片描述
可能在一个线程A中,刚刚将tickets加载到内存上,线程A就被切走了,这时线程A的数据和上下文被保存,线程A从CPU上被剥离。

线程B开始抢票,如果他的竞争力非常强,一次运行后抢到了1000张票。

线程B执行完后线程A又来了,他会从上次执行的地方继续执行,但是他上次保存的tickets的数据是10000,所以抢到了一张票后,将剩余的9999张票写回内存,本来线程B执行完后还剩9000张票,但是线程A执行完后剩余的票数反而增多了。

2.互斥锁(mutex)

对于上面的抢票程序,要想使每个线程正确的抢票就要保证:当一个线程在进入到抢票环节时,其他线程不能进行抢票。
所以就可以对抢票环节加互斥锁。

pthread_mutex_init、pthread_mutex_destroy:对线程锁进行初始化和销毁

#include <pthread.h> 
// pthread_mutex_t mutex: 锁变量,所有线程都可看到 
int pthread_mutex_destroy(pthread_mutex_t *mutex);// 销毁锁 
int pthread_mutex_init(pthread_mutex_t *restrict mutex,constpthread_mutexattr_t *restrict attr);// 初始化锁 
// attr: 锁属性,我们传入空指针就可  
// 如果将锁定义为静态或者全局的,可以使用宏直接初始化,且不用销毁 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock、int pthread_mutex_unlock:对线程进行加锁和解锁

#include <pthread.h> 
int pthread_mutex_lock(pthread_mutex_t *mutex);   
int pthread_mutex_unlock(pthread_mutex_t *mutex); 

对抢票小demo进行加锁

#include<iostream>
#include<unistd.h>
#include<stdio.h>using namespace std; int ticket=10000;  //临界资源
pthread_mutex_t mutex;void* threadRoutinue(void* args) {const char* name=static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);if(ticket>0){usleep(1000);//模拟抢票花费时间cout<<name<<" get a ticket: "<<ticket--<<endl;pthread_mutex_unlock(&mutex);}else{cout<<name<<"票抢完了"<<endl;pthread_mutex_unlock(&mutex);break;}usleep(1000);}return nullptr; }int main() {pthread_mutex_init(&mutex,nullptr);//创建线程模拟抢票pthread_t tid[4];int n=sizeof(tid)/sizeof(tid[0]);for(int i=0;i<n;i++){char buffer[64];snprintf(buffer,sizeof(buffer),"thread_%d",i);pthread_create(tid+i,nullptr,threadRoutinue,buffer);}for(int i=0;i<n;i++){pthread_join(tid[i],nullptr);}pthread_mutex_destroy(&mutex);    return 0; } 

多线程临界资源原子
在这里插入图片描述

细节:
1.凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这是一个规则,不能有例外
2.每一个线程访问临界资源之前,得加锁,加锁本质是给 临界区加锁,加锁的粒度尽量细一些。
3.线程访问临界区的时候,需要先加锁 -> 所以线程都必须看到同一把锁 -> 锁本身就是公共资源 -> 锁如何保证自己的安全? -> 加锁和解锁本身就是原子的。
4.临界区可以是一行代码,可以是一批代码,a.线程可能被切换? 当然可能 b.切换会有影响嘛? 没有,因为一个线程申请一个锁以后,该线程被临时切换,其他任何线程没有办法进入临界区,无法申请到锁,所以无法访问到临界资源。
5.这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态是:锁被申请(持有锁),锁被释放(不持有锁),原子性。

3.互斥锁的原理

以抢票程序为例,当线程需要访问临界资源时,需要先访问mtx,为了所有的线程都能看到它,所以锁肯定是全局的。

且锁本身也是临界资源。那么如何保证锁本身是安全的,即获取锁的过程是安全的。
其原理是:加锁(lock)、解锁(unlock)的过程是原子的!

那怎样才算是原子的呢:一行代码被翻译成汇编后只有一条汇编,就是原子的。

为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令。

该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

在这里插入图片描述

当线程申请到锁之后,进入到临界区访问临界资源,这时线程也可能被切走,被切走后会保护上下文,而锁数据也在上下文中。

所以锁也被带走了,所以即便是该线程被挂起了,其他线程也不能申请到锁,也不能进入临界区。

必须等待拥有锁的线程释放锁之后才能申请到锁。

4.自定义封装一个锁

#pragma once
#include<iostream>
#include<pthread.h>//封装锁class _Mutex
{
public:_Mutex(pthread_mutex_t* mutex):_mutex(mutex){}void lock(){pthread_mutex_lock(_mutex);}void unlock(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t* _mutex;
};class lockGuard
{
public:lockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~lockGuard(){_mutex.unlock();}private:_Mutex _mutex;
};

我们可以使用我们自己封装的锁解决抢票问题
在这里插入图片描述

二、可重入和线程安全

线程安全: 线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

线程安全的实现,通过同步与互斥实现

具体互斥的实现可以通过互斥锁和信号量实现、而同步可以通过条件变量与信号量实现。

常见的线程不安全的情况:

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

可重入与线程安全联系:

  • 函数是可重入的,那就是线程安全的

  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别:

  • 可重入函数是线程安全函数的一种

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数锁还未释放则会产生死锁

三、死锁

死锁概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资 源而处于的一种永久等待状态。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用

  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

如何避免死锁

核心思想:破坏死锁的4个必要条件中任意一个!

  • 不加锁

  • 主动释放锁

  • 按顺序申请锁

  • 资源一次性分配

破坏死锁的一个小demo(主动释放锁)

#include <iostream>
#include<unistd.h>
#include <pthread.h>using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *pthreadRoutinue(void *args) {  pthread_mutex_lock(&mutex); //加锁cout<<"I get a mutex"<<endl;pthread_mutex_lock(&mutex); //产生死锁cout<<"i alive again"<<endl;return nullptr; }int main() {pthread_t pid;pthread_create(&pid, nullptr, pthreadRoutinue, nullptr);sleep(3);cout<<"main thread run"<<endl;pthread_mutex_unlock(&mutex);//主线程区解锁cout<<"main thread unlock"<<endl;sleep(3);return 0; } 

在这里插入图片描述

四、线程同步

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步 。

1.条件变量

概念

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件变量接口

pthread_cond_init、pthread_cond_destroy:初始化、销毁条件变量

#include <pthread.h> 
int pthread_cond_destroy(pthread_cond_t *cond);
// pthread_cond_t:条件变量类型,类似pthread_mutex_t int
pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr);   // 如果是静态或全局的条件变量可使用宏初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

pthread_cond_wait、pthread_cond_signal:等待条件、唤醒线程

#include <pthread.h>   // 等待条件满足 
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);   
// 唤醒一个线程,在cond等待队列里的第一个线程 
int pthread_cond_signal(pthread_cond_t *cond);
// 一次唤醒所有线程 
int pthread_cond_broadcast(pthread_cond_t *cond); ```

demo

#include <iostream>
#include<unistd.h>
#include <pthread.h>using namespace std;
#define num 5int ticket =1000; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;void *active(void *args) {  string name=static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex); //调用该函数,会自己释放锁cout<<name<<" 活动"<<endl;pthread_mutex_unlock(&mutex);}return nullptr; }int main() {pthread_t tids[num];for(int i=0;i<num;i++){char * name=new char[64];snprintf(name,64,"thread-%d",i); //线程创pthread_create(tids+i,nullptr,active,name);}sleep(3);while(true){cout<<"main thread wakeup thread..."<<endl;//pthread_cond_signal(&cond); //唤醒cond队列中的一个线程pthread_cond_broadcast(&cond); //将cond队列中所以线程唤醒sleep(1);}for(int i=0;i<num;i++){pthread_join(tids[i],nullptr); //线程等待}sleep(3);return 0; } 

在这里插入图片描述

基于阻塞队列实现生产者消费者模型

在这里插入图片描述

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型的优点:解耦 支持并发 支持忙闲不均

实则之前所讲的进程间通信中的管道通信就是一种生产者消费者模型,管道就是让不同的进程能够看到同一份资源,且管道自带同步和互斥的机制。进程间通信的本质其实就是生产者消费者模型。

代码:

blockQueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>const int gcap = 5;template <class T> class BlockQueue { public:BlockQueue(const int cap = gcap) : _cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_consumerCond, nullptr);pthread_cond_init(&_productorCond, nullptr);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_consumerCond);pthread_cond_destroy(&_productorCond);}bool isFull(){return _cap == _q.size();}void push(const T &in){ // 生产pthread_mutex_lock(&_mutex);while (isFull())  //细节1:使用while ,防止多线程被唤醒生产过多{   // 我们只能在临界区内部,判断临界资源是否就绪 注定了我们在当前一定是持有锁的pthread_cond_wait(&_productorCond, &_mutex); // 如果队列为满,生产者线程休眠 ,此时持有锁,wait会将锁unlock// 当线程醒来的时候,注定了继续从临界区内部继续运行,因为是在临界区被切走的// 注定了当线程被唤醒的时候,继续在pthread_cond_wait()函数继续向后运行,又要重新申请锁,申请成功才会彻底返回}// 没有满,让他继续生产_q.push(in);//策略,唤醒消费者线程pthread_cond_signal(&_consumerCond);pthread_mutex_unlock(&_mutex);}void pop(T *out){pthread_mutex_lock(&_mutex);while (_q.empty())  //队列为空{pthread_cond_wait(&_consumerCond, &_mutex); }*out = _q.front();_q.pop();//策略,唤醒生产者pthread_cond_signal(&_productorCond);pthread_mutex_unlock(&_mutex);}private:std::queue<T> _q;int _cap;pthread_mutex_t _mutex;pthread_cond_t _consumerCond;  // 消费者对应的条件变量 空 waitpthread_cond_t _productorCond; // 生产者对应的条件变量 满 wait }; ```

task.hpp

#pragma once#include <iostream>
#include <string>class Task { public:Task() {}Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':if(_y==0) _exitCode=-1;else _result = _x / _y;break; case '%':if(_y==0) _exitCode=-1;else _result = _x % _y;break;default:break;}}std::string formatArg(){return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";}std::string formatRes(){return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode; }; ```

main.cc

#include "blockQueue.hpp"
#include"task.hpp"
#include<ctime>
#include<unistd.h>void *consumer(void *args) {BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while(true){sleep(1);Task t;//1.将数据从blockqueue中获取  -- 获取到数据bq->pop(&t);t();//2.结合某种业务逻辑,处理数据!std::cout<<"consumer data: "<<t.formatArg()<<t.formatRes()<<std::endl;} }void *productor(void *args) {srand((uint64_t)time(nullptr)^getpid());BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);std::string opers="+-*/%";while(true){//1.先通过某种渠道获取数据int x=rand()%20+1;int y=rand()%10+1;//2.将数据推送到blockqueue  -- 完成生产过程char op=opers[rand()%opers.size()];Task t(x,y,op);bq->push(t);std::cout<<"productor Task: "<<t.formatArg()<<"?"<<std::endl;} }int main() {//BlockQueue<int> *bq = new BlockQueue<int>();BlockQueue<Task> *bq = new BlockQueue<Task>();// 单生产,单消费  支持多生产,多消费,因为看到同一快资源,使用同一把锁pthread_t c, p;pthread_create(&c, nullptr, consumer, bq);pthread_create(&p, nullptr, productor, bq);pthread_join(c, nullptr);pthread_join(p, nullptr);delete bq;return 0; } ```

运行结果:

在这里插入图片描述

2.信号量

概念

信号量本质就是一个计数器,用来描述临界区中临界资源的数目大小。

临界资源如果可以被划分为更小的资源,如果处理得当,我们也有可能让多个线程同时访问临界资源,从而实现并发。

但是每个线程想访问临界资源,都得先申请信号量资源。

信号量操作接口

申请信号量成功时,临界资源的数目会减一;释放信号量时,临界资源的数目会加一。

由于信号量是用来维护临界资源的,首先必须得保证自身是安全的,所以常规的对全局变量的++或–操作肯定是不行的。

P操作(申请信号量)
V操作(释放信号量)

sem_init、sem_destroy:初始化销毁信号量(具体用法与mutex和cond十分类似)

#include <semaphore.h> 
int sem_init(sem_t *sem, int pshared, unsigned int value); 
// pshared: 默认为0, value:信号量的初始值(count) 
int sem_destroy(sem_t *sem);   
// sem_t :信号量类型 // Link with -pthread. ```

sem_wait、sem_signal: 申请、释放信号量

int sem_wait(sem_t *sem); // P操作  
int
sem_post(sem_t *sem); // V操作    
// Link with -pthread. ```

基于环形队列的生产者消费者模型

在这里插入图片描述
但是现在的环形队列的判空判满不再使用中的两种方式判断,因为有了信号量可以判定。

队列为空的时候,消费者和生产者指向同一个位置。(生产和消费线程不能同时进行)(生产者执行)

队列为满的时候,消费者和生产者也指向同一个位置。(生产和消费线程不能同时进行)(消费者执行)

当队列不为空不为满的时候,消费者和生产者不指向同一个位置。(生产和消费线程可以并发执行)

根据上面三种情况,基于环形队列的生产者消费者模型应该遵守以下规则:

  • 生产者不能把消费者套一个圈

  • 消费者不能超过生产者

  • 当指向同一个位置的时候,要根据空、满状态,判断让谁先执行

  • 其他情况,消费者和生产者可以并发执行

实现:

ringQueue.hpp

#pragma once#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>static const int N = 5;template <class T> class RingQueue { private:void P(sem_t &s)  {sem_wait(&s);}void V(sem_t &s){sem_post(&s);}void Lock(pthread_mutex_t &m){pthread_mutex_lock(&m);}void Unlock(pthread_mutex_t &m){pthread_mutex_unlock(&m);}public:RingQueue(int num = N) : _ring(num), _cap(num){sem_init(&_data_sem, 0, 0);sem_init(&_space_sem, 0, num);_c_step = _p_step = 0;pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);}// 生产void push(const T &in){// 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况// 2. 什么时候用锁,对应的临界资源,是否被整体使用P(_space_sem);  // P() Lock(_p_mutex); _ring[_p_step++] = in;_p_step %= _cap;Unlock(_p_mutex);V(_data_sem);}// 消费void pop(T *out){P(_data_sem);Lock(_c_mutex);*out = _ring[_c_step++];_c_step %= _cap;Unlock(_c_mutex);V(_space_sem);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}private:std::vector<T> _ring;int _cap;         // 环形队列容器大小sem_t _data_sem;  // 只有消费者关心sem_t _space_sem; // 只有生产者关心int _c_step;      // 消费位置int _p_step;      // 生产位置pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex; };

task.hpp

#pragma once#include <iostream>
#include <string>class Task { public:Task() {}Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':if(_y==0) _exitCode=-1;else _result = _x / _y;break; case '%':if(_y==0) _exitCode=-1;else _result = _x % _y;break;default:break;}}std::string formatArg(){return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";}std::string formatRes(){return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode; }; ```

main.cc

#include "ringQueue.hpp"
#include "task.hpp"
#include <ctime>
#include <pthread.h>
#include <memory>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>using namespace std;const char *ops = "+-*/%";void *consumerRoutine(void *args) {RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){Task t;rq->pop(&t);t();cout << "consumer done, 处理完成的任务是: " << t.formatRes() << endl;} }void *productorRoutine(void *args) {RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){// sleep(1);int x = rand() % 100;int y = rand() % 100;char op = ops[(x + y) % strlen(ops)];Task t(x, y, op);rq->push(t);cout << "productor done, 生产的任务是: " << t.formatArg() << endl;} }int main() {srand(time(nullptr) ^ getpid());RingQueue<Task> *rq = new RingQueue<Task>();// 单生产单消费// pthread_t c, p;// pthread_create(&c, nullptr, consumerRoutine, rq);// pthread_create(&p, nullptr, productorRoutine, rq);// pthread_join(c, nullptr);// pthread_join(p, nullptr);//多生产,多消费pthread_t c[3], p[2];for (int i = 0; i < 3; i++)pthread_create(c + i, nullptr, consumerRoutine, rq);for (int i = 0; i < 2; i++)pthread_create(p + i, nullptr, productorRoutine, rq);for (int i = 0; i < 3; i++)pthread_join(c[i], nullptr);for (int i = 0; i < 2; i++)pthread_join(p[i], nullptr);delete rq;return 0; } ```

运行结果

在这里插入图片描述

五、总结

互斥锁与信号量的异同

  • 互斥锁由同一线程加放锁,信号量可以由不同线程进行PV操作。

  • 计数信号量允许多个线程,且值为剩余可用资源数量。互斥锁保证多个线程对一个共享资源的互斥访问,信号量用于协调多个线程对一系列资源的访问条。

条件变量与信号量的异同

  • 使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能。

  • 信号量是有一个值,而条件变量是没有的。从实现上来说一个信号量可以是用mutex + count + cond实现的。因为信号量有一个状态,可以精准的同步,信号量可以解决条件变量中存在的唤醒丢失问题。

  • 条件变量一般需要配合互斥锁使用,而信号量可根据情况而定。

  • 有了互斥锁和条件变量还提供信号量的原因是:尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。信号量最有用的场景是用以指明可用资源的数量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/92502.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入理解React中fiber

一、前言 Fiber是对React核心算法的重写&#xff0c;Fiber是React内部定义的一种数据结构&#xff0c;将更新渲染耗时长的大任务&#xff0c;分为许多的小片。Fiber节点保存啦组件需要更新的状态和副作用&#xff0c;一个Fiber代表一个工作单元。 二、Fiber在React做了什么 …

快速排序与冒泡排序以及代码

快速排序 快速排序&#xff08;Quicksort&#xff09;是一种常用的排序算法&#xff0c;它基于分治的思想。 时间复杂度&#xff1a;O&#xff08;nlogn&#xff09; 空间复杂度&#xff1a;O&#xff08;logn&#xff09; 快速排序的基本思想如下&#xff1a; 选择一个元素…

xxl-job 2.2之后版本高版本executor未授权访问漏洞

xxl-job 低版本executor未授权访问 低版本的executor未授权访问漏洞是 POST /run HTTP/1.1 Host: your-ip:9999 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like G…

【租车骑绿道】python实现-附ChatGPT解析

1.题目 租车骑绿道 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 部门组织绿道骑行团建活动。租用公共双人自行车骑行,每辆自行车最多坐两人、做大载重M。 给出部门每个人的体重,请问最多需要租用多少双人自行车 输入描述 第一行两个数字m、n,自行车限重m,代表部门总…

如何初始化一个vue项目

如何初始化一个vue项目 安装 vue-cli 后 ,终端执行 vue ui npm install vue-cli --save-devCLI 服务 | Vue CLI (vuejs.org) 等一段时间后 。。。 进入项目仪表盘 设置其他模块 项目构建后目录 vue.config.js 文件相关配置 官方vue.config.js 参考文档 https://cli.vuejs.o…

BI神器Power Query(25)-- 使用PQ实现表格多列转换(1/3)

实例需求&#xff1a;原始表格包含多列属性数据,现在需要将不同属性分列展示在不同的行中&#xff0c;att1、att3、att5为一组&#xff0c;att2、att3、att6为另一组&#xff0c;数据如下所示。 更新表格数据 原始数据表&#xff1a; Col1Col2Att1Att2Att3Att4Att5Att6AAADD…

链表的注意事项

LeetCode 19 删除链表的倒数第N个结点 1、sudo.next head;和head sudo.next; 的区别&#xff1f; 他们是不一样的&#xff0c;前者是将head赋给sudo.next&#xff0c;而后者是将sudo.next赋给head。 首先ab&#xff0c;那么b必须存在 我想要将sudo作为虚拟头节点&#xf…

SEO搜索引擎

利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名&#xff0c;吸引更多的用户访问网站&#xff0c;提高网站的访问量&#xff0c;提高网站的销售能力和宣传能力&#xff0c;从而提升网站的品牌效应 搜索引擎优化的技术手段 黑帽SEO 通过欺骗技术和滥用搜索算法来推销毫不…

黑豹程序员-架构师学习路线图-百科:Git/Gitee(版本控制)

文章目录 1、什么是版本控制2、特点3、发展历史4、SVN和Git比较5、Git6、GitHub7、Gitee&#xff08;国产&#xff09;8、Git的基础命令 1、什么是版本控制 版本控制系统&#xff08; Version Control &#xff09;版本控制是一种管理和跟踪软件开发过程中的代码变化的系统。它…

嵌入式Linux应用开发-基础知识-第十八章系统对中断的处理③

嵌入式Linux应用开发-基础知识-第十八章系统对中断的处理③ 第十八章 Linux系统对中断的处理 ③18.5 编写使用中断的按键驱动程序 ③18.5.1 编程思路18.5.1.1 设备树相关18.5.1.2 驱动代码相关 18.5.2 先编写驱动程序18.5.2.1 从设备树获得 GPIO18.5.2.2 从 GPIO获得中断号18.5…

【QT】使用toBase64方法将.txt文件的明文变为非明文(类似加密)

目录 0.环境 1.背景 2.详细代码 2.1 .h主要代码 2.2 .cpp主要代码&#xff0c;主要实现上述的四个方法 0.环境 windows 11 64位 Qt Creator 4.13.1 1.背景 项目需求&#xff1a;我们项目中有配置文件&#xff08;类似.txt&#xff0c;但不是这个格式&#xff0c;本文以…

C#,数值计算——Ranhash的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// High-quality random hash of an integer into several numeric types. /// </summary> public class Ranhash { public Ranhash() { }…

【新版】系统架构设计师 - 未来信息综合技术

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 未来信息综合技术考点摘要信息物理系统CPS的体系架构CPS 的技术体系CPS应用场景 人工智能分类关键技术机器学习 机器人发展分类机器人4.0 边缘计算概念与特点边云协同安全应用场景 数字孪生关键技…

【RuoYi项目分析】介绍RuoYi网关的过滤器

文章目录 1. 网关的过滤器清单2. GatewayFilterFactory和GlobalFilter 的原理3. RuoYi网关的配置文件是如何配置的4. 资料参考 本文主要介绍 RuoYi 中用到的过滤器&#xff0c;以及过滤器原理简单分析。Spring Gateway 的详细原理请参考作者另外的文章。 1. 网关的过滤器清单 …

前端开发和后端开发的一些建议

前端开发和后端开发是Web开发的两个方向 前端开发主要负责实现用户在浏览器上看到的界面和交互体验&#xff0c;包括HTML、CSS和JavaScript等技术。后端开发主要负责处理服务器端的逻辑和数据&#xff0c;包括数据库操作、服务器配置和接口开发等技术。 前端开发 前端开发需…

GitHub上有助于开发微信小程序的仓库

2023年9月30日&#xff0c;周六晚上 最近帮同学在GitHub找了一些开发小程序会用到的东西 目录 UI库WePY框架基于WePY框架的Demo微信小程序开发资源汇总 UI库 GitHub - Tencent/weui-wxss: A UI library by WeChat official design team, includes the most useful widgets/m…

2023年9月随笔之摩托车驾考

1. 回头看 日更坚持了273天。 读《SQL学习指南&#xff08;第3版&#xff09;》更新完成 读《高性能MySQL&#xff08;第4版&#xff09;》持续更新 学信息系统项目管理师第4版系列持续更新 9月码字81307字&#xff0c;日均码字数2710字&#xff0c;累计码字451704字&…

Explain执行计划字段解释说明---ID字段说明

ID字段说明 1、select查询的序列号,包含一组数字&#xff0c;表示查询中执行select子句或操作表的顺序 2、ID的三种情况 &#xff08;1&#xff09;id相同&#xff0c;执行顺序由上至下。 &#xff08;2&#xff09;id不同&#xff0c;如果是子查询&#xff0c;id的序号会…

基于Java的厨艺交流平台设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

【算法分析与设计】贪心算法(下)

目录 一、单源最短路径1.1 算法基本思想1.2 算法设计思想1.3 算法的正确性和计算复杂性1.4 归纳证明思路1.5 归纳步骤证明 二、最小生成树2.1 最小生成树性质2.1.1 生成树的性质2.1.2 生成树性质的应用 2.2 Prim算法2.2.1 正确性证明2.2.2 归纳基础2.2.3 归纳步骤2.3 Kruskal算…