在Linux系统中,如果有多个线程并发运行,就需要考虑线程同步的问题。比如下面一道笔试题:
两个等价线程并发的执行下列程序,a为全局变量,初始为0,假设printf、++、--操作都是原子性的,则输出不肯哪个是(A)
void foo() {
if(a <= 0) {
a++;
}
else {
a--;
}
printf("%d", a);
}
A.01
B.10
C.12
D.22
答案是A。但是为什么会出现像C或D那样的结果呢?
假设两个线程分别为线程1和线程2。线程1先运行,在执行完第三行后,线程2运行,这时候变量a任然是0,所以线程2会执行if语句,之后输出结果“1”,这时候线程1继续运行,继续给变量a加1,之后输出结果“2”,所以就出现了C选项。
在多线程环境中,要确保线程对共享变量的操作结果的确定性。不允许两个以上线程同时修改一个变量,这就涉及到了线程的同步。有以下方法确保线程的同步:
1、互斥量
互斥量本质上是一把锁。要访问公共资源时,先对互斥量加锁,在访问结束后,释放互斥量上面的锁。对互斥量加锁后,如果再有线程对互斥量加锁,这个线程会阻塞,直到当前进程释放互斥量上面的锁。如果有多个线程因为对互斥量二次加锁而阻塞,那么这些线程会同时变为运行,第一个调度的线程再次对互斥量加锁。
要想避免上面的结果,就要确保foo()函数执行的原子性,通过互斥量可以保证。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int a=0;
pthread_mutex_t counter_mutex=PTHREAD_MUTEX_INITIALIZER;//创建互斥量并初始化
void *foo(void *);
int main(int argc, char **argv)
{
pthread_t tidA,tidB;
pthread_create(&tidA, NULL, foo, NULL);//创建线程
pthread_create(&tidB, NULL, foo, NULL);
pthread_join(tidA,NULL);//等待线程执行结束
pthread_join(tidB,NULL);
pthread_mutex_destroy(&counter_mutex);//销毁互斥量
return 0;
}
void *foo(void *vptr)
{
pthread_mutex_lock(&counter_mutex);//给互斥量加锁
if(a<=0){
a++;
}
else{
a--;
}
printf("%d",a);
pthread_mutex_unlock(&counter_mutex);//释放互斥量上面的锁
}
如果想再给互斥量加锁时不要阻塞,可以使用
pthread_mutex_trylock
如果互斥量未加锁,它会给互斥量加锁;如果互斥量已经加锁,它会立刻返回EBUSY。
如果一个线程试图给同一个互斥量加锁2次,那么这个线程会阻塞;如果要给多个互斥量加锁,要确保加锁顺序的一致性,否则很容易造成死锁。
2、条件变量
在多线程中环境中,可能会有这样的情景:线程1要等待一个条件A发生,当条件A发生后,线程1才可以执行;而线程2在执行过程中会使条件A发生。这时就要用到条件变量了。
条件变量常常和互斥量一起使用,因为互斥量要保护条件变量。线程在改变条件变量前,必须给互斥量加锁,防止多个线程同时请求信号量。
借助《Linux C编程一站式学习》的生产者消费者模型来说明:生产者生产货物,把货物放到仓库(一个链表),消费者从仓库消费货物(前提时有货物的情况下);如果没有货物,消费者要等待生产者生产货物这个条件,消费者知道这个条件发生后才可以消费货物。生产者和消费者不能同时使用仓库。
#include<stdlib.h>
#include<pthread.h>
#include<stdio.h>
struct msg{
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product=PTHREAD_COND_INITIALIZER;//初始化条件变量
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//互斥量
void *consumer(void *p)//消费者
{
struct msg *mp;
for(;;){
//对链表进行保护
pthread_mutex_lock(&lock);
while(head==NULL)//无货的话,等待生产者生产
pthread_cond_wait(&has_product,&lock);
mp=head;
head=mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n",mp->num);
free(mp);
sleep(rand()%5);
}
}
void *producer(void *p)//生产者
{
struct msg *mp;
for(;;){
mp=malloc(sizeof(struct msg));
mp->num=rand()%1000+1;
printf("Produce %d\n",mp->num);
//在对链表操作之前保护链表
pthread_mutex_lock(&lock);
mp->next=head;
head=mp;
pthread_mutex_unlock(&lock);
//条件发生
pthread_cond_signal(&has_product);
sleep(rand()%5);
}
}
int main(int argc, char **argv)
{
pthread_t tidA,tidB;
pthread_create(&tidA, NULL, producer, NULL);
pthread_create(&tidB, NULL, consumer, NULL);
pthread_join(tidA,NULL);
pthread_join(tidB,NULL);
return 0;
}
pthread_cond_wait
的第二个参数是锁住的互斥量。这个函数的功能是把线程放到等待队列上然后对互斥量解锁,这两步操作是一个原子操作。如果在这一步不释放锁,那么生产者就无法对链表就行操作。在条件成立后,这个函数返回,同时锁住互斥量。
pthread_cond_timewait
多了个time,可以设定一个时间值,过了这个时间值,条件还没成立也会返回。
pthread_cond_signal(&has_product)
将唤醒等待该条件的某个线程,而
pthread_cond_broadcast(&has_product)
将唤醒等待该条件的所有线程。
3、信号量
首先要说明的是,信号量不仅可以用于线程同步,也可以用于进程间的通信。
信号量和互斥量有点类似,互斥量可以看做是资源可用数,其值为1,当互斥量上锁以后,值为0,资源不可用。信号量可以看做是资源数可以大于1的互斥量。
当线程/进程使用信号时:
(1)测试信号量的值
(2)如果值为正,则该资源可用,给信号量的值减1,占用一个资源
(3)如果为负,则线程/进程进入休眠状态,直至信号量大于0,被唤醒,然后转到第(1)步
还是借助《Linux C编程一站式学习》的生产者消费者模型来说明,这是仓库基于循环队列。
#include<stdlib.h>
#include<pthread.h>
#include<stdio.h>
#include<semaphore.h>
#define NUM 5;
int queue[5];
sem_t blank_number,product_number;
void *producer(void *arg)
{
int p=0;
while(1){
sem_wait(&blank_number);
queue[p]=rand()%1000+1;
printf("Produce %d\n",queue[p]);
sem_post(&product_number);
p=(p+1)%NUM;
sleep(rand()%5);
}
}
void *consumer(void *arg)
{
int c=0;
while(1){
sem_wait(&product_number);//给信号量减1
printf("Consume %d\n",queue[c]);
queue[c]=0;
sem_post(&blank_number);//给信号量加1
c=(c+1)%NUM;
sleep(rand()%5);
}
}
int main()
{
pthread_t pid,cid;
sem_init(&blank_number,0,5);//对于生产者来说,仓库是空的,可以用的有5个
sem_init(&product_number,0,0);//对于消费者来说,仓库无货
pthread_create(&pid,NULL,producer,NULL);
pthread_create(&cid,NULL,consumer,NULL);
pthread_join(pid,NULL);
pthread_join(cid,NULL);
sem_destroy(&blank_number);//销毁信号量
sem_destroy(&product_number);
return 0;
}
sem_trywait
是试图给信号量减1,如果信号量为负,并不会阻塞,而是立即返回。