互斥量
可以用互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量从本质来说就是一把锁,在访问数据的时候进行加锁,阻止其他线程的访问,在访问完成后释放互斥量。
互斥变量是用pthread_mutex_t数据类型表示的。在使用互斥变量之前先进行初始化 如下所示:
pthread_mutex_init 和 pthread_mutex_destory
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//初始化
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//销毁
注意
如果动态分配内存(malloc),在释放内存前需要调用detroy
加锁和解锁互斥变量
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
举例(自己)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int res = 0;
pthread_t ntid;
pthread_t tid[5];
pthread_mutex_t f_lock[5];
void *
add(void *p){
res++;
return ((void *)0);
}
void *
thr_fun(void *arg){
return ((void *)0);
}
int main(){
int err;
for(int i= 0; i < 5; i++){
pthread_mutex_init(f_lock + i, NULL);
pthread_mutex_lock(f_lock + i);
err = pthread_create(&ntid, NULL, add, NULL);
if(err != 0){
err_exit(err, "can't create thread");
}
pthread_mutex_unlock(f_lock + i);
}
sleep(1);
printf("res=%dn", res);
return 0;
}
输出:
解析: 第33行的sleep(1)比不可少,否则可能会导致main进程先结束,main进程一旦结束所有线程都会摧毁。 思路是在调动add函数来进行res的+1操作时先初始化一个互斥变量并且加锁,这样别人就不能对add的地址进行操作的,操作完成之后再释放互斥量锁。
举例(书11.10)
#include <stdlib.h>
#include <pthread.h>
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
/* ... continue initialization ... */
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
if (--fp->f_count == 0) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&fp->f_lock);
}
}
引用计数是一种古老的内存管理计数,很简单,但是非常有效,性能也很高,上面就是一种C语言下的引用计数,当然,我们可以将函数以函数指针的形式放置在结构体中,这里就不弄了。 可以看到,结构体非常简单,就一个引用计数成员、互斥量和数据成员,使用foo_alloc函数分配空间,并且在其中初始化互斥量,在foo_alloc函数中我们并没有使用互斥量,因为初始化完毕前分配线程是唯一的能使用的线程。但是在这里例子中,如果一个线程调用foo_rele释放引用,但是在此期间另一个线程使用foo_hold阻塞了,等第一个线程调用完毕,引用变为0,并且内存被回收,然后就会导致崩溃。 这里参考了网上的资料。
死锁
定义
线程试图对同一个互斥量加锁两次,那么它就会陷入死锁循环。
产生条件
- 线程对同一个互斥量加锁两次
- A和B的锁相互占用。
如何避免
方法1:多次上锁时让上锁的顺序需要和加锁的顺序相同。
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
/* need to recheck the condition */
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
可以看到,在函数foo_alloc中,有这样一段:
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock[&fp->f_lock);
在这里对同一个互斥量上了两次锁,因此解锁时两次锁的顺序需要是相同的。 比较图 11-11 和图 11-10,可以看出,分配函数现在锁住了散列列表锁,把新的结构添加到了散列桶中,而且在对散列列表的锁解锁之前,先锁定了新结构中的互斥量。因为新的结构是放在全局列表中的,其他线程可以找到它,所以在初始化完成之前,需要阻塞其他线程试图访问新结构。 foo_find函数锁住散列列表锁,然后搜索被请求的结构。如果找到了,就增加其引用计数并返回指向该结构的指针。注意,加锁的顺序是,先在foo_find函数中锁定散列列表锁,然后再在foo_hold函数中锁定foo结构中的f_lock互斥量。 现在有了两个锁以后,foo_rele函数就变得更加复杂了。如果这是最后一个引用,就需要对这个结构互斥量进行解锁,因为我们需要从散列列表中删除这个结构,这样才可以获取散列列表锁,然后重新获取结构互斥量。从上一次获得结构互斥量以来我们可能被阻塞着,所以需要重新检查条件,判断是否还需要释放这个结构。如果另一个线程在我们为满足锁顺序而阻塞时发现了这个结构并对其引用计数加1,那么只需要简单地对整个引用计数减1,对所有的东西解锁,然后返回。
这种锁方法很复杂,所以我们需要重新审视原来的设计。我们也可以使用散列列表锁来保护结构引用计数,使事情大大简化。结构互斥量可以用于保护foo结构中的其他任何东西。图11-12反映了这种变化。