当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。
如果每个线程内部的变量其他线程都不会访问到,那么就不存在一致性问题;
如果变量是只读的,那么多个线程同时访问它也不存在不一致性问题;
但是,一旦一个变量是可写,当一个线程对它进行修改的时候,其他有可能对它进行读取或者写入操作从而导致数据不一致的问题。此时就需要同步机制来保证。
APUE上给出一个例子:
由于递增操作不是原子性的,因此不可避免的会出现上述数据不一致的问题。
1.互斥量
我们可以通过pthread库提供的互斥接口来保护数据,确保在同一时间只有一个线程访问这个数据。互斥量(mutex)本质上是一把锁,当我们访问共享资源对互斥量加锁,访问资源对互斥量解锁。对互斥量加锁后,任何试图向对该互斥量加锁的线程都会阻塞直到该锁被释放。
考虑多个线程访问一个数据,每个线程在访问数据之前都会先访问该互斥量,如果该互斥量已经加锁则阻塞,不然就对该互斥量加锁再去访问实际的数据。当有多线程因为互斥量加锁被阻塞时,一旦锁释放了,这些阻塞的线程都会运行起来,此时,第一个线程再去加锁互斥量访问数据,其他线程只能再次等待。
下面是一个关于引用计数的实例,其中count记为引用对象的个数,要求是对于count++,count–必须只有一个线程能访问,且只有当count == 0才释放资源。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
typedef struct Counter
{
int count;
pthread_mutex_t mutex;
}Counter;
void Init(Counter* pc);
void Inc(Counter* pc);
void Dec(Counter* pc);
int main()
{
Counter* pc = (Counter*)malloc(sizeof(Counter));
if (pc == NULL)
exit(-1);
Init(pc);
//...
//...
return 0;
}
void Init(Counter* pc)
{
pc->count = 1;
pthread_mutex_init(&pc->mutex, NULL);
}
void Inc(Counter* pc)
{
pthread_mutex_lock(&pc->mutex);
pc->count++;
pthread_mutex_unlock(&pc->mutex);
}
void Dec(Counter* pc)
{
pthread_mutex_lock(&pc->mutex);
pc->count--;
if (pc->count == 0)
{
pthread_mutex_unlock(&pc->mutex);
pthread_mutex_destroy(&pc->mutex);
free(pc);
}
else
{
pthread_mutex_unlock(&pc->mutex);
}
}
关于死锁
如果一个线程试图对同一个互斥量加两次锁则会造成死锁。当然还有其他不明显的方式可能导致死锁,考虑以下的情况,线程A占有第一个互斥量,线程B占有第二个互斥量,线程A试图去占有第二个互斥量而出于阻塞,线程B试图去占有第一个互斥量时也处于阻塞,这样子一来,A在等待B释放锁,B也在等待A释放锁,相互阻塞,结果是谁也释放不了锁,造成死锁。
死锁一般要满足四个条件:
- 互斥
- 不可抢占
- 占有并等待
- 循环等待
上述解除死锁的方法实际上就是破坏这几个其中的一个,比如用trylock函数如果不能获得锁就释放锁,这就是破坏第三个条件,而按照锁的先后次序去得到锁则是破坏了第四个条件。
具体可参见死锁,银行家算法
2.条件变量
条件变量是线程可用的另一种同步机制,条件变量和互斥量一起使用,允许线程以无竞争的方式等待特定条件的发生。