信号量
1、信号量的引入
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
// global shared varible
volatile int cnt = 0; // counter
// thread routine
void *thread(void* arg)
{
int i, niters = *((int*)arg);
for(int i = 0; i < niters; ++i)
cnt++;
return NULL;
}
int main(int argc, char* argv[])
{
int niters;
pthread_t tid1, tid2;
if(argc != 2)
{
printf("usage: %s <niters>\n", argv[0]);
exit(0);
}
niters = atoi(argv[1]);
// create thread and wait for them to finish
pthread_create(&tid1, NULL, thread, &niters);
pthread_create(&tid2, NULL, thread, &niters);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("cnt = %d\n", cnt);
return 0;
}
当输入的 niters 足够大的时候,发现 cnt 并不等于 2 * niters
:
[root@SuperhandsomeChuan synchronization]# ./a.out 100000000
cnt = 127028782
[root@SuperhandsomeChuan synchronization]# ./a.out 200000000
cnt = 255307600
问题就处在,我们无法预测线程的执行顺序
2、PV操作
信号量 s
是具有 非负整数值的全局变量 ,只能由两种特殊的操作来处理,这两种操作称为 Р
和 V
:
P(s)
:如果 s 是非零的,那么 P 将 s 减 1,并且立即返回。如果 s 为零,那么就挂起这个线程,直到 s 变为非零,而一个 V 操作会重启这个线程。在重启之后, Р 操作将 s 减 1,并将控制返回给调用者。V(s)
: V 操作将 s 加 1。如果有任何线程阻塞在 Р 操作等待s变成非零,那么 V 操作会重启这些线程中的一个,然后该线程将 s 减 1,完成它的 Р 操作。
P 中的 测试和减 1 操作是不可分割的,也就是说,一旦预测信号量 s 变为非负,就会将 s 减 1,不能有中断。V 中的 加 1 操作和测试 也是不可分割的,也就是 加载、加 1 和存储信号量 的过程中没有中断。注意,V 的定义中没有定义等待线程被重新启动的顺序。唯一的要求是 V 必须 只能重启一个 正在等待的线程。因此,当有多个线程在等待同一个信号量时,你不能预测 V 操作要重启哪一个线程。
理解了 PV 操作,我们就可以修改 1 中的程序了。
首先了解一下 POSIX semaphore 接口:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
我们来封装一下 PV 操作:
void P(sem_t* s) {sem_wait(s);}
void V(sem_t* s) {sem_post(s);}
修改后的程序:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<semaphore.h>
// global shared varible
volatile int cnt = 0; // counter
sem_t mutex;
void P(sem_t* s) {
sem_wait(s);}
void V(sem_t* s) {
sem_post(s);}
// thread routine
void *thread(void* arg)
{
int i, niters = *((int*)arg);
for(int i = 0; i < niters; ++i)
{
P(&mutex);
cnt++