以下提到的几种应用方式,下面都有示例代码。
注意:有个点容易遗忘的:当semop的实参sops设置>0的操作时,一般要给这个op动作添加SEM_UNDO标志,详情可参考另一篇博文:linux线程通信之信号量。
应用情景一:用信号量打造一个二值信号量(互斥量),也即:任何时刻只允许一个线程访问共享资源。P操作用于占用资源,V操作代表释放资源。
使用信号量,关键是要知道semop函数的特性:
① semop函数的第二形参sops可以以数组地址的形式输入多个动作(称为动作集),man手册上讲,semop函数会按照sops数组的顺序、原子性的执行动作集中的动作,要么一个都不执行,要么全部执行。手册上说的这句话我觉得是有点问题的,“要么全部执行”这句话实际上有个例外:如果动作集中某个动作设置的条件(如等待0)会使得线程堵塞在本函数中(或者本函数出错返回)的话,那么后面的动作就只能等解除堵塞之后才能被执行(堵塞时),或者得不到执行(semop出错返回时)。
② 如果在某时刻有多个线程都在等待互斥信号量的使用权,那么一旦占用该互斥量的线程把它释放后,这多个等待的线程中,只能有一个线程被解除堵塞
ps:当无法获得信号量资源时,semop到底是堵塞,还是设置错误并返回,取决于第四参数是否或了IPC_NOWAIT标志。
方法1:
步骤:
(1) 把信号量的值初始化为0(创建信号量之后默认值就是0,该步骤不做也行)
(2) P操作,用semop函数设置线程等待信号量的值semval为0,若不为0则堵塞或报错;然后用semop函数把信号量的值+1(也即:semval为0时可以立即通过,否则就要等待)。本步骤中的两个动作,必须通过semop的实参一次把两个动作都输入进去,而不能分别调用两次semop来实现。
(3) V操作,用semop函数设置信号值-1,注意:只有信号量值≥abs(-1)时,才能够立即减1后立即返回,否则本线程又得等待,直到信号量值≥abs(-1)。当然,因为P操作已经把信号量值+1了,所以这里信号量值肯定是≥abs(-1)。
分析:①整个程序中首次执行P操作的时候,情况是怎样的?看步骤(2),设置本线程为等待semval变为0,因为semval被初始化为0了,所以semop会立即返回或者继续执行形参指定的下一个动作:把semval+1。+1这种动作永不堵塞,于是本线程将继续向下执行开始访问共享资源。
②当某个线程A执行P之后,尚未V之前,又有另个线程B开始执行P了,情况是怎样的?还是看步骤(2),设置本线程B为等待semval变为0,因为线程A已经把semval设为1了,于是线程B被堵塞或报错。
③ 为什么步骤(2)中不允许把等待0和+1这两个动作分别用两次semop来实现?试想这样一种情形:semval初始化为0,当进程A等待0时,发现确实是0,于是继续向下执行semop的+1(进而开始访问共享资源),这时发生了进程/线程调度,切入了线程B,线程B恰好也要执行P操作,等待semval变为0,也发现确实是0,于是继续向下执行semop的+1 (进而开始访问共享资源),显然,没有达到预想的互斥的效果。semop函数提供了一种机制,把多个动作(称为动作集)通过形参一次性传入进去之后,操作系统可以保证,这些动作要么一个也不执行,要么全部被执行(除非:如果某个动作设置的条件会堵塞线程时,等堵塞解除后,后面的动作才会执行),这就杜绝了这一问题。
方法2:
步骤:
① 用semctl或者semop把信号量值初始化为1
② P操作,用semop函数设置线程等待,直到semval≥abs(-1)才解除等待(也即,semval≥1时可以立即通过,否则就要等待);
③ V操作,用semop函数设置semval+1
分析:①整个程序中首次执行P操作的时候,情况是怎样的?看步骤(2),因为semval被初始化为1了,故本次P操作并不堵塞或出错,而是把semval-1后直接返回,P操作完成后semval就变成0了;
②当某个线程A执行P之后,尚未V之前,又有另个线程B开始执行P了,情况是怎样的?还是看步骤(2),设置本线程B为等待等待semval≥abs(-1),因为线程A的P已经把semval设为0了,于是线程B被堵塞或报错;
应用情景二:例如,某个资源最多只允许5个线程同时访问
这种应用场景的一个更贴近生活的例子:某开水房的水管上(这跟水管就是个共享资源),只有5个水龙头,那么这跟水管最多只允许5个人同时打水。每来一个人打水,信号量减1,每走一个人,信号量+1,也即:只要空闲水龙头的数目(信号量)≥1,就可以放人进来打水,否则,都得排队等。
这种应用情景的处理方法,和上面提到的方法(2)是一样的,唯一的区别就是初始化时,要信号量的值初始化为n:
方法3:
步骤:
① 用semctl或者semop把信号量值初始化为5;
② P操作,用semop函数设置线程等待,直到semval≥abs(-1)才解除等待(也即,semval≥1时可以立即通过,否则就要等待);
③ V操作,用semop函数设置semval+1;
分析: 程序把信号量值初始化为5以后,同时有13个线程发起了P操作,请求访问共享资源,这时情况是怎样的?名义上是同时,实际上在该信号量的信息维护链表中,发起P操作的线程仍然是有先后的,第一名开始执行P,信号量发现自己的值是5,可以满足第一名提出的semval≥abs(-1)无需等待条件,于是第一名无需等待,P操作直接返回(<的操作返回时会把信号量值减掉),从而可以访问共享资源了;第二名线程开始执行P操作,信号量发现自己是4,也可以满足不等待的条件semval≥abs(-1)······,也即,前5名线程执行P操作,完全不用等待,都可以直接获得共享资源,而其余的13-7=7个线程执行P操作会被阻塞。前5名当中,一旦有其中一个用完了资源并释放了资源(执行V操作)之后,那么第6名线程就会解除等待,从而获得共享资源的访问权。
示例代码:
由以上分析可见,方法3已经把方法2的情形包含进去了,这里只给出方法1和方法3的实例代码:
运行结果分析: