POSIX线程-条件变量(二)

pthread_cond_signal()pthread_cond_wait()的典型使用方法

pthread_mutex_t mtx;

pthread_cond_t cond;

线程A中通知的执行的操作

  1. s = pthread_mutex_lock(&mtx)

  2. if (s != 0)

  3. err_exit();

  4. a++;/*谓词条件*/

  5. ……

  6. do some work/*处理共享的数据*/

  7. …….

  8. s = pthread_mutex_unlock(&mtx)

  9. if (s != 0)

  10. err_exit();

  11. pthread_cond_signal(&cond);

 

线程B执行被唤醒的操作

  1. s= pthread_mutex_lock(&mtx);

  2. if (s != 0)

  3.         err_exit();

  4. /*在锁住互斥量之后和在等待条件变量之前,测试谓词是很重要的*/

  5. while (a < 0/*此处要检查共享变量的状态/*)

  6.         pthread_cond_wait(&cond, &mtx);

  7. a--;

  8. /*处理共享数据 */

  9. ……………….

  10. s = pthread_mutex_unlock(&mtx)

  11. if (s != 0)

  12.         err_exit();

在线程B中为什么把pthread_cond_wait()放在while循环中?很多人不能理解。大部分人认为把pthread_cond_wait()放在if中就行了。这是因为他们没有理解到,pthread_cond_wait()会苏醒的条件,在《POSIX多线程程序设计》中有很详细的说明,现简约摘录:

1.   被拦截的唤醒:也就是说可能有多个线程被唤醒(被pthread_cond_broadcast唤醒),被唤醒的线程都“争着”去处理共享数据,但只有一个线程能处理到这个共享数据,等它处理完毕后,其他线程也会进入该临界区去处理共享数据,这个时候就会出现共享数据被多次处理的情况,我们的解决方法是在被唤醒后,在去检查一下谓词变量,看该共享数据是否已经被处理了。

2.    松散的谓词:有时候谓词表示了“可能有工作”而不是“有工作”的意思。所以,当线程被唤醒,我们必须确认到底有没有工作。

3.    假唤醒:在有些系统中,我们没有用pthread_cond_signal()pthread_cond_broadcast()来唤醒用pthread_cond_wait()休眠的线程,但是在休眠的线程还是会被唤醒,这是怎么回事呢?在linux中,被休眠的进程或线程,都有被信号(linux中的信号)打断的可能,也就是说,如果线程或进程在休眠中,会有被唤醒的可能,所以我们在有可能引起休眠的API调用返回后都会检查一下返回值是否为EINT,如果是,那么线程或进程都将继续休眠。

为什么不把线程A中的pthread_cond_signal()放在pthread_mutex_unlock()前面?

当一个等待线程被唤醒的时候,它必须首先加锁互斥量(参见pthread_cond_wait()执行步骤)。如果线程被唤醒而此时通知线程任然锁住互斥量,则被唤醒线程会立刻阻塞在互斥量上,等待通知线程解锁该互斥量,引起线程的上下文切换。当通知线程解锁后,被唤醒线程继续获得锁,再一次的引起上下文切换。这样导致被唤醒线程不能顺利加锁,延长了加锁时间,加重了系统不必要的负担。

pthread_cond_wait()中为什么需要把mtx传递进去?

先考虑没有mtx参数的pthread_cond_wait(), 我们是否可以像下面这样等待“信号”的到来

pthread_mutex_lock(&mtx);

       while(检查谓词)

 pthread_cond_wait(&cond);

pthread_mutex_unlock(&mtx);

上面这种方式在休眠的时候,没有把互斥量释放掉,将会导致通知线程无法获得互斥量,导致死锁。那把pthread_cond_wait(&cond)放在pthread_mutex_lock(&mtx)之前又怎么样呢?

while(检查谓词)

pthread_cond_wait(&cond);

pthread_mutex_lock(&mtx);

pthread_mutex_unlock(&mtx);

看起来好像没什么问题。考虑下面这种情况:

T3:线程C发现a等于0,执行pthread_cond_wait(),线程休眠。

T1:a++,谓词改变。

T2:线程B发现a大于0了,执行pthread_mutex_lock(),由于此时线程A持有互斥量,线程B进入阻塞。

T4:线程A完成了共享数据的操作,执行pthread_cond_signal()唤醒所有等待的线程。

T5:线程C被唤醒,进入临界区操作共享数据,操作完毕后退出。

T6:线程B获得了互斥量,进入临界区操作共享数据,此处会产生错误,重复操作了共享数据!!

造成这种情况的原因是因为对a的访问没有互斥量的保护,导致线程C和线程B读到的a都是不相同的。所以,要把互斥量放在pthread_cond_wait()中,在还没休眠前,锁定a的状态不被其他线程修改。

 

我们来看看pthread_cond_wait()的源代码

  1. int

  2. __pthread_cond_wait (cond, mutex)

  3.      pthread_cond_t *cond;

  4.      pthread_mutex_t *mutex;

  5. {

  6.   struct _pthread_cleanup_buffer buffer;

  7.   struct _condvar_cleanup_buffer cbuffer;

  8.   int err;

  9.  

  10.   /* Make sure we are along. */

  11.   lll_mutex_lock (cond->__data.__lock);

  12.  

  13.   /* Now we can release the mutex. */

  14.   err = __pthread_mutex_unlock_usercnt (mutex, 0);

  15.   if (__builtin_expect (err, 0))

  16.     {

  17.       lll_mutex_unlock (cond->__data.__lock);

  18.       return err;

  19. }

  20. …………………………….

  21. ……….操作该cond的某些字段……

  22.   ++cond->__data.__total_seq;

  23.   ++cond->__data.__futex;

  24.   cond->__data.__nwaiters += 1 << COND_NWAITERS_SHIFT;

  25. ……………………………………….

  26.   do

  27.     {

  28.       unsigned int futex_val = cond->__data.__futex;

  29.  

  30.       /* Prepare to wait. Release the condvar futex. */

  31.       lll_mutex_unlock (cond->__data.__lock);

  32.  

  33.       /* Enable asynchronous cancellation. Required by the standard. */

  34.       cbuffer.oldtype = __pthread_enable_asynccancel ();

  35.  

  36.       /* Wait until woken by signal or broadcast. */

  37.       lll_futex_wait (&cond->__data.__futex, futex_val);

  38.  

  39.       /* Disable asynchronous cancellation. */

  40.       __pthread_disable_asynccancel (cbuffer.oldtype);

  41.  

  42.       /* We are going to look at shared data again, so get the lock. */

  43.       lll_mutex_lock (cond->__data.__lock);

  44.  

  45.       /* If a broadcast happened, we are done. */

  46.       if (cbuffer.bc_seq != cond->__data.__broadcast_seq)

  47.        goto bc_out;

  48.  

  49.       /* Check whether we are eligible for wakeup. */

  50.       val = cond->__data.__wakeup_seq;

  51.     }

  52.   while (val == seq || cond->__data.__woken_seq == val);

  53. ………………………………….

  54.   /* We are done with the condvar. */

  55.   lll_mutex_unlock (cond->__data.__lock);

  56.  

  57.   /* The cancellation handling is back to normal, remove the handler. */

  58.   __pthread_cleanup_pop (&buffer, 0);

  59.  

  60.   /* Get the mutex before returning. */

  61.   return __pthread_mutex_cond_lock (mutex);

  62. }

首先锁住cond中的锁,然后释放掉作为参数传递进来的互斥量。和在POSIX线程-条件变量(一)说的一样。

pthread_cond_wait()函数最开始的两个语句能不能交换呢?

  lll_mutex_lock (cond->__data.__lock);

err = __pthread_mutex_unlock_usercnt (mutex, 0);

 

我们把  pthread_cond_signal()和pthread_cond_wait()的典型使用方法 这段中线程B执行

被唤醒的操作简化和抽象一下:

mtx的操作简化为LOCK, UNLOCK;

cond中锁的操作简化为__lock, __unlock.

括号中的操作代表了pthread_cond_wait()中锁的操作。

 

抽象的结果为(整个序列表示为M):

LOCK

{

 __lock

 UNLOCK

……….操作该cond的某些字段……

__unlock

Wait

LOCK

}

UNLOCK

 

 

 

交换①②顺序的结果为(整个序列表示为N)

LOCK

{

 UNLOCK

 __lock

……….操作该cond的某些字段……

__unlock

Wait

LOCK

}

UNLOCK

 

  在N中,②①之间可能会被打断,比如通知线程在②①之间已经发出了“信号”,但N还没有在休眠队列上准备好;那么,当CPU切换到N中,继续执行①后面的语句时,将会造成N的永久等待(因为“信号”被丢失了)。


  在M中就不会有这种情况发生了么?在M中①②之间也可能被打断,但当①执行之后,cond已经被锁定,通知线程在①②之间不可能发出“信号”(因为pthread_cond_signal()也需要锁定cond)。那么该信号只有在M完成了对cond的某些字段的更新(相当于把该线程挂入了休眠队列),释放了cond上的锁后,才有机会被发送。通过将解锁操作与等待条件变量原子化,确保了在释放互斥量和等待条件变量之间没有线程可以改变谓词。

你可能感兴趣的:(posix)