互斥锁pthread_mutex_t与pthread_cond_wait的使用

来源:互联网 发布:10 100base tx端口 编辑:程序博客网 时间:2024/06/05 18:33

http://blog.chinaunix.net/uid-9543173-id-3579371.html

一、互斥锁的使用

1、互斥锁创建
有两种方法创建互斥锁,静态方式和动态方式。
a)、POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:
    pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
b)、
动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)

其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
2、
pthread_mutex_destroy ()用于注销一个互斥锁,API定义如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
3、
互斥锁属性
   
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
  * PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  * PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  * PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  * PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
4、
锁操作
   
锁操作主要包括加锁 pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
  int pthread_mutex_lock(pthread_mutex_t *mutex)
  int pthread_mutex_unlock(pthread_mutex_t *mutex)
  int pthread_mutex_trylock(pthread_mutex_t *mutex)
  pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

看到这里,读者一定会好奇:Mutex的两个基本操作lock和unlock是如何实现的呢?假设Mutex变量的值为1表示互斥锁空闲,这时某个进程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被某个线程获得,其它线程再调用lock只能挂起等待。那么lock和unlock的伪代码如下:

lock:      if(mutex > 0){          mutex = 0;          return 0;      } else          挂起等待;      goto lock;    unlock:      mutex = 1;      唤醒等待Mutex的线程;      return 0;

unlock操作中唤醒等待线程的步骤可以有不同的实现,可以只唤醒一个等待线程,也可以唤醒所有等待该Mutex的线程,然后让被唤醒的这些线程去竞争获得这个Mutex,竞争失败的线程继续挂起等待。

细心的读者应该已经看出问题了:对Mutex变量的读取、判断和修改不是原子操作。如果两个线程同时调用lock,这时Mutex是1,两个线程都判断mutex>0成立,然后其中一个线程置mutex=0,而另一个线程并不知道这一情况,也置mutex=0,于是两个线程都以为自己获得了锁。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。现在我们把lock和unlock的伪代码改一下(以x86的xchg指令为例):

lock:      movb $0, %al      xchgb %al, mutex      if(al寄存器的内容 > 0){          return 0;      } else          挂起等待;      goto lock;    unlock:      movb $1, mutex      唤醒等待Mutex的线程;      return 0;  
      unlock中的释放锁操作同样只用一条指令实现,以保证它的原子性。

也许还有读者好奇,Linux 中“挂起等待”和“唤醒等待线程”的操作如何实现?

每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

二、线程间同步pthread_cond_wait

多线程的条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
      为了防止竞争,条件变量
的使用总是和一个互斥锁结合在一起。

1. 创建和注销
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)

2. 等待和激发
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待条件有两种方式:条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()、pthread_cond_timedwait()的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(thread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
3、处理流程
pthread_cond_wait使用前必须由本线程调用pthread_mutex_lock加锁
pthread_mutex_lock(&mtx);pthread_cond_wait(&cond,&mtx);pthread_mutex_unlock(&mtx);
pthread_cond_wait内部处理流程:解锁--->阻塞休眠--->唤醒--->加锁

示例:

现在来看一段典型的应用:看注释即可。
#include <pthread.h>#include <unistd.h>#include <stdio.h>#include <string.h>#include <stdlib.h>static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;struct node{    int n_number;    struct node *n_next;} *head=NULL;/*[thread_func]*//*释放节点内存 */static void cleanup_handler(void*arg){    printf("Cleanup handler of second thread.\n");    free(arg);    (void)pthread_mutex_unlock(&mtx);}static void*thread_func(void*arg){    struct node *p = NULL;    pthread_cleanup_push(cleanup_handler, p);    while (1)    {        pthread_mutex_lock(&mtx);        //这个mutex_lock主要是用来保护wait等待临界时期的情况,        //当在wait为放入队列时,这时,已经存在Head条件等待激活的条件,此时可能会漏掉这种处理        //这个while要特别说明一下,单个pthread_cond_wait功能很完善,        //为何这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线程可能会被意外唤醒,如果这个时候head != NULL,        //则不是我们想要的情况。这个时候,应该让线程继续进入pthread_cond_wait        while (head ==NULL)        {            pthread_cond_wait(&cond,&mtx);            // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,然后阻塞在等待队列里休眠,直到再次被唤醒            //(大多数情况下是等待的条件成立而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,            // 再读取资源 用这个流程是比较清楚的/*unlock-->block-->wait() return-->lock*/        }        p = head;        head = head->n_next;        printf("Got %d from front of queue\n", p->n_number);        free(p);        pthread_mutex_unlock(&mtx);//临界区数据操作完毕,释放互斥锁    }    pthread_cleanup_pop(0);    return 0;    /*EC_CLEANUP_BGN (void)pthread_mutex_unlock(&mtx); EC_FLUSH("thread_func") return 1; EC_CLEANUP_END*/}int main(void){    pthread_t tid;    int i;    struct node *p;    pthread_create(&tid,NULL, thread_func,NULL);    //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大 /*[tx6-main]*/    for (i= 0; i< 10; i++)    {        p = (struct node*)malloc(sizeof(struct node));        p->n_number= i;        pthread_mutex_lock(&mtx);//需要操作head这个临界资源,先加锁,        p->n_next= head;        head = p;        pthread_cond_signal(&cond);        pthread_mutex_unlock(&mtx);//解锁        sleep(1);    }        printf("thread 1 wanna end the cancel thread 2.\n");    pthread_cancel(tid);    //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,    //子线程会在最近的取消点,退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。    pthread_join(tid,NULL);    printf("All done -- exiting\n");    return 0;/*[]*//*EC_CLEANUP_BGN return EXIT_FAILURE; EC_CLEANUP_END*/}
http://blog.csdn.net/yusiguyuan/article/details/14160081