一、为何需要信号量
信号量用来干嘛的呢?搜寻答案的话,很多人都会告诉你主要用于线程同步的,意思就是线程通信的。简单来说,比如我运行了2个线程A和B,但是我希望B线程在A线程之前执行,那么我们就可以用信号量来处理。有些人可能会疑惑,那么麻烦干嘛?你不是要B线程先执行吗?那么我让A线程休眠一点时间不就可以了吗?没错,这个思路是可以的,但是如果B线程也因为某些原因(比如硬件,操作系统的原因)导致延缓执行了,这该怎么办?到底A线程该休眠多少时间合适呢?所以正确的做法就是在B线程阻塞,A线程去唤醒这个阻塞线程。
看到这儿,看过我前面文章的朋友可能一眼就看出来了这个不就是前面讲的生产消费者模型提到的用法吗?
没错,信号量的实现也是靠条件变量和互斥锁。
所以虽然C++中并没有在语言级别上支持信号量,但同样的我们可以利用以上两个来自己实现一个。
这里我也不得不提一句,条件变量和互斥锁组合使用真的非常强大,生产消费者模型中用到了,线程池中用到了,现在说的信号量也用到了,所以大家一定要好好掌握条件变量和互斥锁的使用,它们俩是你在多线程世界中纵横捭阖的利剑。
二、信号量的实现
那么我们如何用C++来实现一个信号量呢?
#ifndef _SEMAPHORE_H
#define _SEMAPHORE_H
#include <mutex>
#include <condition_variable>
using namespace std;class Semaphore
{
public:Semaphore(long count = 0) : count(count) {}//V操作,唤醒void signal(){unique_lock<mutex> unique(mt);++count;if (count <= 0)cond.notify_one();}//P操作,阻塞void wait(){unique_lock<mutex> unique(mt);--count;if (count < 0)cond.wait(unique);}private:mutex mt;condition_variable cond;long count;
};
#endif
信号量里面用到了一个叫PV操作的东西,P操作时阻塞,一般用wait()函数,V操作是唤醒,一般用singal()函数,至于不叫WS操作,反而为什么叫PV操作呢?网上说是因为提出这一系统方法的人狄克斯特拉用荷兰文定义的,因为在荷兰文中,通过叫passeren,释放叫vrijgeven,PV操作因此得名。对我们来说,这些也没有太大的意义,记住这些定义就好了,毕竟定义这种东西,是不以我们的意志为转移的。
写好了信号量的接口,那我们如何使用这个信号量呢?这个就需要我们在外部写一个多线程的调用函数来调用。
#include "semaphore.h"
#include <thread>
#include <iostream>
using namespace std;Semaphore sem(0);void funA()
{sem.wait();//do somethingcout << "funA" << endl;
}void funB()
{this_thread::sleep_for(chrono::seconds(1));//do somethingcout << "funB" << endl;sem.signal();
}int main()
{thread t1(funA);thread t2(funB);t1.join();t2.join();
}
三、信号量解析
这里我们想让funB线程运行,然后再运行funA,多线程是通过时间片轮询来执行的。
假设先开始跑funA,执行到sem.wait()的时候,进入wait函数可知,count减1,小于0,会发生阻塞,等待其他线程唤醒。
然后就会切换到funB,这里即使休眠了1秒也不会切换到funA,因为那边阻塞了,没有其他线程唤醒的话就会一直阻塞。funB休眠完之后,就会打印出结果,然后执行sem.signal(),进入signal函数可知,count加1,小于等于0,会唤醒其他阻塞的线程。
然后再切到funA,执行后面的操作,打印出结果。
所以这个就一定能保证funB先执行,funA后执行。当然前提是初始化信号量对象的时候,要初始化为0。
Semaphore sem(0);
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。像这里funB完成任务之后就通过信号量的PV操作告诉funA线程可以开始任务了。
最后需要注意的是,信号量不仅可以用于进程也可用于线程,它比条件变量要复杂很多,条件变量仅限于线程内使用,至于进程间如何使用信号量通信,后期我们在讨论。