当多个控制线程共享相同的内存时,要确保对数据访问的正确性,就需要做线程的同步与互斥工作。先看下面这个例子:
#include
#include
#include
#include
#include
size_t count = 0;
void ModifyCount()
{
++count;
}
void* ThreadEntry(void *arg)
{
(void)arg;
for(size_t i = 0; i < 10000; ++i){
ModifyCount();
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, ThreadEntry, NULL);
pthread_create(&tid2, NULL, ThreadEntry, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("count: %ld\n", count);
return 0;
}
我们让两个线程同时对一个全局变量做 ++
操作,各自增加 10000次,如果程序执行无误,结果应该是 20000,实际上运行结果如下:
我们让该程序执行了多次,发现只有两次的结果正确,而其他的结果都超出了我们的预期,原因在哪呢?
我看看看++
操作的汇编代码:
对 count 的增加有三步:
- 将 count 移至寄存器中
- 给寄存器中的值加1
- 再将寄存器中的值移回count
显然,对count的增加并不是一个原子操作,如果两个线程同时访问count,就可能会出现下面这种情况:
- 线程1将count的值移至寄存器(count = 0,eax1 = 0);
- 线程1对寄存器中的值++(count = 0, eax1 = 1);
- 线程2拿到count的值(count = 0, eax2 = 0);
- 线程1将寄存器中的值写回count(count = 1,eax1 = 1);
- 线程2对寄存器中的值++(count = 1, eax2 = 1);
- 线程2将寄存器中的值写入count(count = 1, eax2 = 1)。
此时,看似两个线程对 count 各加了一次,实际上,count 的值只被加了一次,如何解决这种问题呢?下面介绍几种方法。
可以使用 pthread
的护齿接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完之后解锁。对互斥量进行加锁之后,任何试图对互斥量加锁的操作都会被阻塞,直到当前线程释放该互斥锁。如果释放互斥量时,有一个以上的线程被阻塞,那么该锁上的线程都会变为运行状态,其中第一个变为运行状态的线程就可以申请到互斥量并对其加锁。
互斥量的初始化
方法一,静态分配:
pthread_mutex mutex = PTHREAD_MUTEX_INITIALIZER;
方法二,动态分配:
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
其中 pthread_mutex_t
是一个表示互斥量的联合体类型,函数的第一个参数是要初始化的互斥量,第二个参数是要设置的互斥量的属性,参数二为空则表示使用默认的属性。
静态初始化的互斥量不需要销毁,而对于动态初始化的互斥量则需要销毁,其接口函数如下:
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数为要销毁的互斥量,上面的初始化和销毁的函数执行成功返回 0,出错返回错误码。
下面是对互斥量的加锁和解锁函数:
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
第一个函数 pthread_mutex_lock
对互斥量阻塞式加锁,如果互斥量已经上锁,则调用线程将则阻塞式等待,直到互斥量被解锁。pthread_mutex_unlock
用于解锁,如果不希望阻塞式的加锁,则使用第二个函数 pthread_mutex_trylock
,该函数尝试对互斥量加锁,如果互斥量未被上锁,则将互斥量锁住,否则,该函数返回 EBUSY
。
下面是对加了锁的临界资源访问的一个实例:
#include
#include
#include
#include
#include
pthread_mutex_t lock;//定义互斥量
size_t count = 0;
void ModifyCount()
{
pthread_mutex_lock(&lock);
++count;
pthread_mutex_unlock(&lock);
}
void MyHandler()
{
ModifyCount();
}
void* ThreadEntry(void *arg)
{
(void)arg;
for(size_t i = 0; i < 10000; ++i){
ModifyCount();
}
return NULL;
}
int main()
{
pthread_mutex_init(&lock, NULL);//初始化互斥量
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, ThreadEntry, NULL);
pthread_create(&tid2, NULL, ThreadEntry, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("count: %ld\n", count);
pthread_mutex_destroy(&lock);//销毁互斥量
return 0;
}
我们在对 count
做 ++
操作前锁住互斥量,操作完成后解锁互斥量,这样就不会在多个线程修改共享资源时出现错误了。
线程死锁
想想下面这种情况:
线程(也可以是进程)A,B各需要申请 1,2两个互斥量,此时,线程 A 得到了 1 号互斥量(即线程A已对1号互斥量加锁),它在等待互斥量 2,而线程 B 获取到了 2 号互斥量 ,它在等待 1 号互斥量,A,B两个线程既在等待对方占用的互斥量,又在占用着对方所需要的互斥量,由于这种情况造成的两个(或多个)线程都无法前进的情况称为死锁。
产生死锁的必要条件
- 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放;
- 请求保持:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源;
- 不可剥夺:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放;
- 环路等待:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请地资源。
银行家算法避免死锁
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。
条件变量是线程同步可用的另一种同步机制。当某一条件满足时,线程 A 可以通知阻塞在条件变量上的线程 B , B 所期望的条件已经满足,可以解除在条件变量上的阻塞操作,继续做其他事情。
条件变量 API
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:成功0,失败返回错误码
第一个函数用于初始化一个条件变量,参数一为你要初始化的条件变量,参数二为条件变量的属性,为空表示使用默认属性。第二个函数用于销毁一个条件变量。
最后一行是另一种初始化条件变量的方法,这种方法用于静态分配条件变量。
#include
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
返回值:成功0,失败返回错误码
第一个函数中参数 cond
为要等待的条件变量,参数 mutex
用于保护条件变量,调用者将锁住的互斥量传给函数。函数把调用线程放到等待线程的列表上,然后对互斥量解锁,这两个操作时原子操作。pthread_cond_wait
返回时再次锁住互斥量。第二个函数与第一个类似,知识多了一个指定等待时间的参数 timeout
。
#include
int pthread_cond_signal(pthread_cond_t *cond);
该函数向线程或条件变量发送信号,用于唤醒等待的线程,告诉该线程你等待的条件已经成熟。
下面是一个实例:
#include
#include
#include
pthread_cond_t g_cond;
pthread_mutex_t g_lock;
void* Entry1(void *arg)
{
(void)arg;
while(1){
printf("pass!\n");
pthread_cond_signal(&g_cond);//当传球以后再通知投球
usleep(678123);
}
}
void* Entry2(void *arg)
{
(void)arg;
while(1){
pthread_cond_wait(&g_cond, &g_lock);//等待别人给自己传球,若条件成熟,则下一步投球
printf("shoot!\n");
usleep(123456);
}
}
int main()
{
pthread_cond_init(&g_cond, NULL);//初始化条件变量
pthread_mutex_init(&g_lock, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, Entry1, NULL);
pthread_create(&tid2, NULL, Entry2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_cond_destroy(&g_cond);
pthread_mutex_destroy(&g_lock);
return 0;
在篮球场上,投球的人需要等到别人将球传给自己,而传球的人要通知投球的人“我将球传给你了,你准备投,他们按照这样约定的顺序,擦能实现有序配合”,下面是运行结果:
POSIX信号量也可以用于保护共享资源,以达到对共享资源的互斥访问。与 systemV
信号量不同的是:
POSIX信号量来源于POSIX技术规范的实时扩展方案 (POSIX Realtime Extension),常用于线程;system v信号量,常用于进程的同步。这两者非常相近,但它们使用的函数调用各不相同。前一种的头文件为
semaphore.h
,函数调用为sem_init()
,sem_wait()
,sem_post()
,sem_destory()
等等。后一种头文件为,函数调用为
semctl()
,semget()
,semop()
等函数.
POSIX信号量API:
初始化和销毁信号量:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
sem
为要初始化的信号量,pshared
为 0 表示线程间共享,非 0 表示进程间共享。
value
表示要初始化为几?
等待信号量和发送信号量:
#include
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
如果将信号量看为一个计数器的话,那么对信号量的等待和唤醒则是对计数器的 -1
和 +1
操作。
下面是用 POSIX
信号量重新实现的上面那个“传球-投篮”的例子:
#include
#include
#include
#include
sem_t sem;
void* Entry1(void *arg)
{
(void)arg;
while(1){
printf("pass!\n");
sem_post(&sem);
usleep(678123);
}
}
void* Entry2(void *arg)
{
(void)arg;
while(1){
sem_wait(&sem);
printf("shoot!\n");
usleep(123456);
}
}
int main()
{
sem_init(&sem, 0, 1);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, Entry1, NULL);
pthread_create(&tid2, NULL, Entry2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem);
return 0;
}
——完!
【作者:果冻:http://blog.csdn.net/jelly_9】