使用POSIX Threads进行多线程编程(三) ——条件变量

来源:互联网 发布:淘宝订单贷款能用多久 编辑:程序博客网 时间:2024/06/06 01:10

说明:

  1. 本文是翻译自《MultiThreaded-Programming-With-POSIX》,作者Guy Kerens。
  2. 本文预计翻译三章,主要涉及pthread基本知识互斥量(锁)条件变量,一是因为这已经能够引导读者入门,二是因为本人在工作之余翻译,实在时间捉急。
  3. 翻译:张小川,转载请保留原作者

精致同步——条件变量

如前所说的互斥量,他们允许简单的同步——对资源的互斥访问。然而,我们经常会需要在线程间作真正的同步:

  1. 在一个服务器,一个线程读取用户的请求,并调度几个线程来处理。在有数据需要处理的时候,这些线程需要被告知,不然的话他们就应该处于等待状态(不消耗CPU资源);
  2. 在一个GUI(Graphical User Interface,图形用户接口)应用中,一个线程读取用户输入,一个线程处理图形输出,第三个线程向服务器发送请求并处理回复。服务器处理线程需要在服务器回复后通知画图线程,所以画图线程就可以即使显示给用户。用户输入线程需要一直等待用户输入,例如,取消当前服务器线程的长时间耗时操作。

所有这些例子都需要在条件变量间发送通知的能力,这就引入了条件变量。

什么是条件变量

条件变量(condition variable)是一种允许线程等待(不消耗CPU)某些事件发生的机制。几个线程可能会等待一个条件变量,直到其他线程告知(signals)这个条件变量(发送一个通知)。这时,等待这个条件变量线程中的一个被唤醒,并继续活动。可以使用一个广播(broadcast)方法唤醒所有等待该条件变量的线程。

需要注意的是,一个条件变量并不提供锁。因此,在使用条件变量时总是需要互斥量来为访问这个条件变量提供必要的锁。

创建和初始化条件变量

通过定义一个pthread_cond_t类型的变量来创建一个条件变量,并正确地初始化。初始化可以通过一个名为PTHREAD_COND_INITIALIZER的宏或者使用pthread_cond_init()函数。第一种初始化方法示例如下:

pthread_cond_t got_request = PTHREAD_COND_INITIALIZER;

这句定义了一个名为got_request的条件变量,并初始化。

注意:因为PTHREAD_COND_INITIALIZER是一个结构体初始化值,它只能在一个条件变量声明的时候用来初始化它,如果想要在运行时初始化它,就必须使用pthread_cond_init()函数。

通知(signaling)一个条件变量

我们可以使用pthread_cond_signal()函数来通知一个条件变量(只唤醒等待该条件变量所有线程中的一个),或者使用函数pthread_cond_broadcast()来通知一个条件变量(唤醒等待该条件变量的所有线程)。如下给出了使用通知的一个例子(假定got_request是一个已经正确初始化的条件变量):

int rc = pthread_cond_signal(&got_request);

使用广播函数

int rc = pthread_cond_broadcast(&got_request);

上述两个函数成功时返回值‘rc’为0,不成功返回非零值。在失败时,返回值表示了发生的错误(EINVAL表示输入参数不是一个条件变量,ENOMEM表示系统内存耗尽)。

注意:通知操作的返回成功并不表示一定有线程被唤醒——可能是没有线程等待这个条件变量,因此通知操作不做任何事(i.e. thesignal is lost)。并且该唤醒操作并不被记忆以备后用——如果在通知函数返回后,另一个线程开始等待这个条件变量,那么就需要再来一个通知来唤醒这个线程。

等待一个条件变量

如果一个线程通知条件变量,其他的线程可能会等待这个通知。他们可能通过pthread_cond_wait()pthread_cond_timewait()两个函数中的一个来进行等待。这两个函数的参数都是一个条件变量和一个互斥量(在调用等待函数之前应该是锁定状态),解锁互斥量,并等待直到接到条件变量的通知,并且挂起线程的执行。如果这个通知使得线程得到唤醒(看之前关于pthread_cond_signal()的讨论),等待函数会使互斥量自动锁定,然后等待函数返回。

两个等待函数之间唯一的区别在于pthread_cond_timewait()允许程序员为等待指定一个超时限制(timeout),在这个时间之后,函数会返回一个适当的错误值(ETIMEDOUT)来说明在超时之前条件变量没有得到通知。而pthread_cond_wait()如果没有通知的话则会无限期等待。

如下给出了使用这两个函数的示例。假定got_request是一个已经正确初始化的条件变量,request_mutex是一个正确初始化的互斥量。首先,尝试下pthread_cond_wait()函数:

/*first lock the mutex*/int rc = pthread_mutex_lock(&request_mutex);if( rc ){    //error    perror("pthread_mutex_lock");    pthread_exit(NULL);}/*mutex is now locked - wait on the condition variable*//*during the execution of pthread_cond_wait, the mutex is unlocked*/rc = pthread_cond_wait(&got_request, &request_mutex);if (rc == 0){    /* we were awakened due to the cond. variable being signaled */    /* The mutex is now locked again by pthread_cond_wait() */    /* do your stuff... */}/* finally, unlock the mutex */pthread_mutex_unlock(&request_mutex);

下面给出了一个使用函数pthread_cond_timewait()的函数:

#include<sys/time.h> /* struct timeval definition */#include<unistd.h>   /* declaration of gettimeofday() */struct timeval now; //time when we started waitingstruct timespec timeout; //time out value for the wait functionint done;               //are we done waiting//first lock the mutexint rc = pthread_mutex_lock(&a_mutex);if(rc){    //error    perror("pthread_mutex_lock");    pthread_exit(NULL);}//mutex is now lockedgettimeofday(&now);//prepare timeout valuetimeout.tv_sec = now.tv_sec + 5;timeout.tv_nsec = now.tv_usec * 1000; /*timeval uses microseconds*/                                      /*timespec uses nanoseconds*/                                      /*1000 nanosecond = microsecond*/// wait on the condition variable// we use a loop, since the unix signal might stop the wait before the timeoutdone = 0;while(!done){    //remember that pthread_cond_timewait() unlocks the mutex on entrance    rc = pthread_cond_timewait(&got_request, &request_mutex, &timeout);    switch(rc)    {        case 0:         {            /* we were awakened due to the cond. variable being signaled */            /* the mutex was now locked again by pthread_cond_timedwait. */            /* do your stuff here... */            done = 0;            break;        }        case ETIMEOUT:        {            //our time is up            done = 0;            break;        }        default:            /* some error occurred (e.g. we got a Unix signal) */            break;          /* break this switch, but re-do the while loop. */    }}/* finally, unlock the mutex */pthread_mutex_unlock(&request_mutex);

如上所示,时间等待版本要复杂得多,因此打包在某些函数内比较好,而非在每个必要为之都重新编码一遍。

注意:有两个或者更多线程在等待的条件变量即使被通知了多次,线程中的一个仍然可能会一直等待不被唤醒。这是因为在条件变量被通知时,我们并不保证哪一个等待的线程会被唤醒。可能被唤醒的线程很快又处于等待状态,而在响应条件变量再次通知的时候再次被唤醒,这样一直下去。被唤醒线程所处的状态叫做“饥饿状态”。如果这种现象会引起程序不良行为的话那么程序员应该保证这种现象不会发生。然而,在我们的服务器例子中,这个现象表明请求来的比较缓慢,因此我们有太多线程等待服务器请求了。这个例子中,这种现象是对的,它表明每个请求在其到来时都被及时处理。

注意2:当互斥量被广播时(使用pthread_cond_broadcast),这并不意味着所有线程都同时开始跑。每个线程在从等待函数返回时都会尝试锁住这个条件变量,然后他们会一个一个地跑,每一个会锁住互斥量,做该线程自己的工作,然后在其他线程有机会跑之前释放锁。

销毁一个条件变量

在我们使用完条件变量后,我们应该销毁它以释放其所占用的系统资源。这可以使用pthread_destroy()函数。这个函数工作的前提是没有线程在等待这个条件变量了。如下展示了这个函数的用法,我们再次假定got_request是一个事先初始化的条件变量:

int rc = pthread_cond_destroy(&got_request);if(rc == EBUSY){    /* some thread is still waiting on this condition variable */    /* handle this case here... */}

如果仍有线程在等待这个条件变量怎么办?视情况而定,这有可能是这个变量使用不当,或者仅仅缺少适当的线程清理代码。这种现象最好通知程序员,至少是在debug阶段。它可能什么也不会影响,但是也有可能影响巨大。

条件变量的实际条件

关于条件变量需要注意的是——他们在没有与之相伴的实际的条件检查时通常是无意义的。为了讲清楚这个,考虑之前介绍的服务器的例子。假设我们使用条件变量got_request来通知一个新的请求来到需要处理,这个请求被保存到一个请求队列里。如果我们有线程在等待这个条件变量,那么肯定会有一个线程被唤醒来处理这个请求。

然而,如果在一个新的请求到来时所有线程都正忙于处理之前的请求怎么办呢?条件变量的通知什么也不会做(因为所有线程都正忙于其他事,没有在等待这个条件变量),在所有线程处理完当前的请求后,他们返回等待条件变量的状态,然而却不一定会再次得到通知(例如,没有新的请求到来)。那么,至少会有一个请求处于等待状态,同时所有的线程都处于等待状态,等待被通知。

为了解决这个问题,我们可以设置一些整数变量来表示等待的请求的数目,让每个线程再等待之前都检查一下这个值。如果这个值是正的,有请求处于等待状态,线程应该处理它,而不是去等待下一个通知。进一步来说,一个线程再处理了一个请求后,应该将该值减1,保证其正确性。

我们来看一下这是如何影响上面所示的等待程序的:

/* number of pending requests, initially none */int num_requests = 0;/* first, lock the mutex */int rc = pthread_mutex_lock(&request_mutex);if (rc) {     /* an error has occurred */    perror("pthread_mutex_lock");    pthread_exit(NULL);}/* mutex is now locked - wait on the condition variable *//* if there are no requests to be handled. */rc = 0;if (num_requests == 0){    rc = pthread_cond_wait(&got_request, &request_mutex);}if (num_requests > 0 && rc == 0) {     /* we have a request pending */    /* unlock mutex - so other threads would be able to handle */    /* other reqeusts waiting in the queue paralelly. */    rc = pthread_mutex_unlock(&request_mutex);    /* do your stuff... */    /* decrease count of pending requests */    num_requests--;    /* and lock the mutex again - to remain symmetrical,. */    rc = pthread_mutex_lock(&request_mutex);}/* finally, unlock the mutex */pthread_mutex_unlock(&request_mutex);

使用条件变量——一个完整例子

作为一个使用条件变量的实际例子,我们将给出一个程序模拟我们之前提到的服务器——一个线程,作为接收端,接收用户请求。它将用户请求插入一个链表,一系列线程,作为处理器,来处理这些请求。为了简便,在我们的模拟中,接收端自己创建请求而非从实际用户读取请求。

程序源代码在thread_pool_server.c,包含许多注释。请先阅读源代码,燃尽后在阅读下面的说明:

  1. main函数首先启动处理线程,然后通过它的主循环来担任接收端线程;
  2. 使用了一个互斥锁,一来保护条件变量,二来保护链表中等待的请求。这简化了设计,作为一个练习,你可以想一下怎么将这部分工作分给两个锁;
  3. 互斥量本身必须是一个递归锁。为了说明原因,看函数handle_requests_loop 。你会发现,它首先会锁住互斥量,然后调用get_request函数,这个函数会再次锁住互斥量。如果使用一个非递归锁,那么在函数get_request中互斥量就会永久被锁定;
  4. 作为一个规则,当使用递归锁时,我们应该保证在同一个函数中每个锁定操作都伴随着一个解锁操作。不然的话,在锁定互斥量几次之后,很难保证会解锁同样的次数,这时就会发生死锁现象;
  5. 调用函数pthread_cond_wait()函数时隐含的解锁和重新锁定互斥量一开始是容易困惑的。最好是在代码中添加一个注释,否则其他人读到这段代码时可能会不小心再添加一个锁定操作;
  6. 当一个处理线程处理一个请求时——它应该释放互斥锁,以避免阻塞所有其他的处理线程。在它处理完这个请求之后,它应该再次锁定这个互斥量,并检查是否有更多的请求需要处理。

参考:《MultiThreaded-Programming-With-POSIX》
备注:所有的代码都可以在上述参考中直接找到,就不贴在这儿了

0 0
原创粉丝点击