POSIX信号量用于同步操作,达到无冲突访问临界资源的目的,可以用于线程之间的通信。而信号量的本质其实就是一把计数器!!而我们对计数器有2个操作,一个是增加计数器的值,一个是减少计数器的值。
而减少值的操作我们称之为P操作。增加值的操作我们称之为V操作
而信号量的P,V操作都是原子的!当信号量计数为0时执行P操作。那么该线程就会进入等待,直到信号量计数不为0才会继续唤醒。而此时另一个线程就可以对信号量进行V操作,让计数器++,从而唤醒之前进入等待的线程。
从本质上说,P操作就是从临界资源拿数据,V操作就是往临界资源放数据!!而又因为P,V操作都是原子的。所以整个过程是线程安全的!!
创建信号量
sem_t x //sem_t 变量名,创建信号量
信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数一: 信号量的地址
参数二: 选项,0为线程信号量,非0为进程信号量
参数三: 信号量的值(信号量本身是一把计数器)
返回值: 成功返回0,失败返回 -1 或者 错误码
信号量的销毁
int sem_destroy(sem_t *sem);
参数: 要销毁的信号量的地址
返回值: 成功返回0,失败返回 -1 或者 错误码
信号量的P操作
信号量的P操作是对信号量的计数进行-- ,当-到0时线程会挂起等待。直到非0后再继续往后执行。
int sem_wait(sem_t *sem);
信号量的V操作
信号量的V操作是对信号量的计数进行++。
int sem_post(sem_t *sem);
查看信号量计数的值
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类中。
然后观察程序。
代码:
#include
#include
#include
#include
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张票转眼间就被抢光了。然后每生产一张票,抢一张票,这个过程是同步的。
信号量和条件变量的区别在哪呢??
本质区别就是信号量知道临界资源的情况!!
而条件变量并不知道临界资源的情况!!所以使用条件变量时,必须要先对临界资源做检测!!
而信号量,P,V操作结束之后就确保一定能访问临界资源!!
条件变量要先对临界资源做检测才能访问临界资源!!
所以,如果是信号量的时候,加锁加在哪。如果是条件变量的时候,加锁加在哪里?
因为信号量知道临界资源的情况,所以P,V操作之后是一定可以访问临界资源的,而PV操作本身又是原子的。所以可以加锁加在P,V操作之前。当然也可以把P,V操作放在加锁和解锁之间,但这样并不建议。因为抢信号量本身是一个占坑的过程,本身就是一种预定机制,并且这个占坑的过程还是原子的。如果把占坑的这个过程放在加锁和解锁之间,就相当于一个有十个坑的厕所。但必须一个一个的去上。
再打个比方:
在电影院中,是在大厅买票后,再进入小门排队进去看电影好呢。还是直接进入小门排队买票再进去好呢?毫无疑问,肯定是大厅买票后再排队好,因为在大厅卖票速度很快,而排队就会轻松很多。而如果进小门排队买票,那么原本可以在好几个地方都可以买票,也就是说可以几个人同时买票。而现在只能在一个地方买票,只能一个人一个人买票,买票效率大大降低。
所以信号量放在加锁前,就是先在大厅买票,之后加锁就是进入小门排队。这样可以同时多个人买票后再排队
放在加锁后就是直接进入小门买票,这样就只能同时一个人买票。
信号量放在加锁前,提升效率!
而条件变量并不知道临界资源的情况,所以要对临界资源做检测。而对临界资源做检测就一定要访问临界资源!!而这个时候就必须要加锁。所以条件变量必须要在加锁和解锁之间!!
如果条件变量wait不放在加锁和解锁之间,那是很容易造成死锁的。放在加锁之前被唤醒后必定死锁!!因为wait在等待时会释放锁,而此时你根本没有锁,所以释放了个寂寞。可是当在条件变量wait中的线程被唤醒时,可是会重新获得锁的,而你此时又去加锁。那恭喜你!死锁在等着你!!