多线程编程之POSIX信号量

POSIX信号量

POSIX信号量用于同步操作,达到无冲突访问临界资源的目的,可以用于线程之间的通信。而信号量的本质其实就是一把计数器!!而我们对计数器有2个操作,一个是增加计数器的值,一个是减少计数器的值。

而减少值的操作我们称之为P操作。增加值的操作我们称之为V操作

而信号量的P,V操作都是原子的!当信号量计数为0时执行P操作。那么该线程就会进入等待,直到信号量计数不为0才会继续唤醒。而此时另一个线程就可以对信号量进行V操作,让计数器++,从而唤醒之前进入等待的线程。

从本质上说,P操作就是从临界资源拿数据,V操作就是往临界资源放数据!!而又因为P,V操作都是原子的。所以整个过程是线程安全的!!

POSIX信号量的接口

创建信号量

c 复制代码
sem_t x //sem_t 变量名,创建信号量

信号量初始化

c 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数一: 信号量的地址
参数二: 选项,0为线程信号量,非0为进程信号量
参数三: 信号量的值(信号量本身是一把计数器)
返回值: 成功返回0,失败返回 -1 或者 错误码

信号量的销毁

c 复制代码
int sem_destroy(sem_t *sem);
参数: 要销毁的信号量的地址
返回值: 成功返回0,失败返回 -1 或者 错误码

信号量的P操作

信号量的P操作是对信号量的计数进行-- ,当-到0时线程会挂起等待。直到非0后再继续往后执行。

c 复制代码
int sem_wait(sem_t *sem);

信号量的V操作

信号量的V操作是对信号量的计数进行++。

c 复制代码
int sem_post(sem_t *sem);

查看信号量计数的值

c 复制代码
 int sem_getvalue(sem_t *sem, int *sval);
第一个参数传信号量的地址
第二个参数是一个输出型参数,返回信号量的值

信号量的简单运用

因为信号量是一把计数器!!所以我们要有线程对计数器进行--的同时,也必须有线程对计数器进行++。否则一旦计数-到0时。那么就线程就会进入等待,直到 计数器 > 0 时才会被唤醒。

而我们可以对 P,V操作的频率进行控制,当 P操作快,V操作慢时。P操作最终会等待V操作。当V操作快,P操作慢时。那计数器会越涨越多。 所以一般都是 P 操作快,V操作慢。而P是拿数据对应的是消费者,V是放数据对应的生产者。

以下图是 P操作快,V操作慢的情况。所以当信号量计数为0时,P操作会等待V操作。

那么接下来我们来设计一个抢票程序。程序流程大概如下:

首先,创建一个信号量sem_tickets,并初始化计数器为500。说明一开始上了500张票。

随后创建4个线程,1个线程生产票,3个线程进行抢票。 为了方便给线程命名,我们把信号量和线程名封装到一个ThreadData类中。

然后观察程序。

代码:

c 复制代码
#include <pthread.h>
#include <string>
#include <semaphore.h>
#include <unistd.h>

int tickets = 500;  //初始信号量的值
//线程数量
#define Thread_num 4

//存储线程的线程名和信号量
class ThreadData
{
public:
    ThreadData(const std::string& name,sem_t* sem_tickets) : _name(name),_sem_tickets(sem_tickets){}
    std::string _name; 
    sem_t *_sem_tickets;
};

//抢票线程执行逻辑
void* BuyTicket(void* args)
{
     ThreadData* td = (ThreadData*)args;
     while(true)
     {
        usleep(1000); //抢票还是不要抢太快,1000微秒抢一张比较好
        sem_wait(td->_sem_tickets); // P 操作 ,对计数器进行--
        int x;
        sem_getvalue(td->_sem_tickets,&x);
        printf("%s 抢了一张票,还剩下 : %d\n",td->_name.c_str(),x);
     }
}


//放票线程执行逻辑
void* PutTicket(void* args)
{
     ThreadData* td = (ThreadData*)args;
     while(true)
     {
        sleep(1); //每隔1秒进行一次V操作
        sem_post(td->_sem_tickets); // V 操作 ,对票数++
        int x; //接收getvalue函数,获取信号量的计数器
        sem_getvalue(td->_sem_tickets,&x);
         printf("%s 放了一张票,票数 : %d\n",td->_name.c_str(),x);
     }
}

int main()
{
    pthread_t tids[Thread_num]; //开四个线程,三个线程抢票,一个线程放票
    sem_t sem_tickets; //创建信号量
    sem_init(&sem_tickets,0,tickets); //初始化信号量
    for(int i = 0 ; i < Thread_num ; i ++)
    { 
        if(i == 0) //第0个线程放票,其他线程抢票
        {
            //创建放票线程
            std::string name = "放票 thread " + std::to_string(i + 1);
            ThreadData* td = new ThreadData(name,&sem_tickets);
            pthread_create(tids+i, nullptr,PutTicket,(void*)td);
        }
        else{
            //创建抢票线程
            std::string name = "抢票 thread " + std::to_string(i + 1);
            ThreadData* td = new ThreadData(name,&sem_tickets);
            pthread_create(tids+i, nullptr,BuyTicket,(void*)td);
        }
    }
	//线程等待
    for(int i = 0 ; i < Thread_num; i ++)
    {
        pthread_join(tids[i],nullptr);
    }
    //销毁信号量
    sem_destroy(&sem_tickets);
    return 0 ;
}

运行结果:

我们可以看到,一开始的500张票转眼间就被抢光了。然后每生产一张票,抢一张票,这个过程是同步的。

信号量 VS 条件变量

信号量和条件变量的区别在哪呢??

本质区别就是信号量知道临界资源的情况!!

而条件变量并不知道临界资源的情况!!所以使用条件变量时,必须要先对临界资源做检测!!

而信号量,P,V操作结束之后就确保一定能访问临界资源!!

条件变量要先对临界资源做检测才能访问临界资源!!

所以,如果是信号量的时候,加锁加在哪。如果是条件变量的时候,加锁加在哪里?

因为信号量知道临界资源的情况,所以P,V操作之后是一定可以访问临界资源的,而PV操作本身又是原子的。所以可以加锁加在P,V操作之前。当然也可以把P,V操作放在加锁和解锁之间,但这样并不建议。因为抢信号量本身是一个占坑的过程,本身就是一种预定机制,并且这个占坑的过程还是原子的。如果把占坑的这个过程放在加锁和解锁之间,就相当于一个有十个坑的厕所。但必须一个一个的去上。

再打个比方:

在电影院中,是在大厅买票后,再进入小门排队进去看电影好呢。还是直接进入小门排队买票再进去好呢?毫无疑问,肯定是大厅买票后再排队好,因为在大厅卖票速度很快,而排队就会轻松很多。而如果进小门排队买票,那么原本可以在好几个地方都可以买票,也就是说可以几个人同时买票。而现在只能在一个地方买票,只能一个人一个人买票,买票效率大大降低。

所以信号量放在加锁前,就是先在大厅买票,之后加锁就是进入小门排队。这样可以同时多个人买票后再排队

放在加锁后就是直接进入小门买票,这样就只能同时一个人买票。

信号量放在加锁前,提升效率!

而条件变量并不知道临界资源的情况,所以要对临界资源做检测。而对临界资源做检测就一定要访问临界资源!!而这个时候就必须要加锁。所以条件变量必须要在加锁和解锁之间!!

如果条件变量wait不放在加锁和解锁之间,那是很容易造成死锁的。放在加锁之前被唤醒后必定死锁!!因为wait在等待时会释放锁,而此时你根本没有锁,所以释放了个寂寞。可是当在条件变量wait中的线程被唤醒时,可是会重新获得锁的,而你此时又去加锁。那恭喜你!死锁在等着你!!

相关推荐
喜欢打篮球的普通人8 分钟前
rust高级特征
开发语言·后端·rust
代码小鑫1 小时前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
豌豆花下猫1 小时前
REST API 已经 25 岁了:它是如何形成的,将来可能会怎样?
后端·python·ai
喔喔咿哈哈1 小时前
【手撕 Spring】 -- Bean 的创建以及获取
java·后端·spring·面试·开源·github
夏微凉.2 小时前
【JavaEE进阶】Spring AOP 原理
java·spring boot·后端·spring·java-ee·maven
彭亚川Allen2 小时前
数据冷热分离+归档-亿级表优化
后端·性能优化·架构
Goboy2 小时前
Spring Boot 和 Hadoop 3.3.6 的 MapReduce 实战:日志分析平台
java·后端·架构
不会编程的懒洋洋3 小时前
Spring Cloud Eureka 服务注册与发现
java·笔记·后端·学习·spring·spring cloud·eureka
NiNg_1_2344 小时前
SpringSecurity入门
后端·spring·springboot·springsecurity
Lucifer三思而后行4 小时前
YashanDB YAC 入门指南与技术详解
数据库·后端