APUE——pthread_cond_wait深度分析

參考鏈接1
源碼1
驚羣效應

1、pthread_cond_wait與signal函數

#include <pthread.h>

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 timeout );

兩者的返回值都是:若成功則返回0,否則返回錯誤編號
#include <pthread.h>

int pthread_cond_signal( pthread_cond_t *cond );

int pthread_cond_broadcast( pthread_cond_t *cond );

兩者的返回值都是:若成功則返回0,否則返回錯誤編號
複製代碼

傳遞給pthread_cond_wait的互斥量對條件進行保護,調用者把鎖住的互斥量傳給函數。函數把調用線程放到等待條件的線程列表上,然後對互斥量解鎖,這兩個操作是原子操作。這樣就關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道,這樣線程就不會錯過條件的任何變化。pthread_cond_wait返回時,互斥量再次被鎖住。
pthread_cond_signal函數將喚醒等待該條件的某個線程
pthread_cond_broadcast函數將喚醒等待該條件的所有線程。
POSIX規範爲了簡化實現,允許pthread_cond_signal在實現的時候可以喚醒不止一個線程。
這裏與我之前分析過的linux內核wait的實現方式不盡相同,內核wait的喚醒wakeup是喚醒最新加入wq上的_waiter,一次喚醒一個,如果喚醒會先再次加到等待隊列,如果condition滿足,則break,且從等待隊列刪除附上鍊接
這裏要注意,mutex保護的條件並不是函數的參數cond,而是while循環判斷的條件,while (workq == NULL),cond本身只是pthread_cond_t 變量,當pthread_cond_signal運行時,會喚醒等待在這個cond上的一個線程,如果在多處理器中可能喚醒多個線程產生驚羣。

2、深入分析

如下代碼是apue的例子

#include <pthread.h>
struct msg {
  struct msg *m_next;
  /* value...*/
};

struct msg* workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void
process_msg() {
  struct msg* mp;
  for (;;) {
    pthread_mutex_lock(&qlock);
    while (workq == NULL) {
      pthread_cond_wait(&qread, &qlock);
    }
    mq = workq;
    workq = mp->m_next;
    pthread_mutex_unlock(&qlock);
    /* now process the message mp */
  }
}

void
enqueue_msg(struct msg* mp) {
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    /** 此時另外一個線程在signal之前,執行了process_msg,剛好把mp元素拿走*/
    pthread_cond_signal(&qready);
    /** 此時執行signal, 在pthread_cond_wait等待的線程被喚醒,
         但是mp元素已經被另外一個線程拿走,所以,workq還是NULL ,因此需要繼續等待*/
}

pthread_cond_wait分析需要解決如下幾個問題

  1. pthread_cond_wait()爲什麼必須要加while循環
  2. pthread_cond_signal之前的條件爲什麼要上鎖
  3. pthread_cond_wait的優勢是什麼
    上面這幾個問題是我在看apue的時候不解的,後續在查閱資料和自己分析之後,得出如下結論。

2.1 pthread_cond_wait()爲什麼必須要加while循環

pthread_cond_wait函數的實現是

  1. 加入等待隊列
  2. 釋放鎖
  3. 調度
  4. 獲得鎖
    當pthread_cond_wait(cond,mutex)之後,被調度走,此時是被lock的狀態,當signal(cond)運行,可能會產生驚羣效應,因爲posix爲了實現簡單,在多處理函數中,會將多個等待cond的線程從等待隊列移除,進入就緒態。那麼如果用if,則都會跳出循環,而用while會加鎖後再次判斷是不是需要的條件!==cond本身只是標誌,實際的條件還是while循環的判斷條件!==附上man鏈接pthread_cond_signal(3) - Linux man page
	thread1:
    if (0<a<10) {
      pthread_cond_wait(&qread, &qlock);
    }
    thread2:
    if (0<a<5) {
      pthread_cond_wait(&qread, &qlock);
    }
    pthread_mutex_lock(&qlock);
    a=6;
    pthread_mutex_unlock(&qlock);
    /** 此時另外一個線程在signal之前,執行了process_msg,剛好把mp元素拿走*/
    pthread_cond_signal(&qready);

這裏線程1和2都wait在cond上,但是這個pthread_cond_signal本身只是爲了喚醒線程1!

2.2 pthread_cond_signal之前的條件爲什麼要上鎖

	thread1															thread2
	pthread_mutex_lock(&qlock);
    while (workq == NULL) {
														       	//pthread_mutex_lock(&qlock);
														  	workq == (void*)1;
														    //pthread_mutex_unlock(&qlock);
														    pthread_cond_signal(&qready);
      pthread_cond_wait(&qread, &qlock);
   
    }
    pthread_mutex_unlock(&qlock);

如上述代碼所示,如果線程2沒有對條件變量加鎖,則線程2可能在線程1進入wait之前,發送信號,線程1還沒有進入等待隊列,所以信號丟失了!
這裏實際可以理解爲與讀寫鎖類似,讀寫是互斥的,線程2是寫,線程1爲讀

2.3 pthread_cond_wait的優勢是什麼

在以前分析線程池的時候,寫過這種代碼

thread1
        pthread_mutex_lock(&mut_num);
        while(num == 0)
        {
            pthread_mutex_unlock(&mut_num);
            sched_yield();  //schedule
            pthread_mutex_lock(&mut_num);
        }
        if(num>0)
        {
            i = num;
            num = 0;
            pthread_mutex_unlock(&mut_num);
            fprintf(stderr,"the num of thread %d,and the %d th  is %d\n",pthread_self(),(int)i);
        }
        else
        {
            pthread_mutex_unlock(&mut_num);//這裏要注意,如果在臨界區,跳轉出臨界區需要解鎖再跳轉
            break;
        }


這裏如果起了4個線程,且num == 0時,這cpu在4個線程之間來回調度,不停的解鎖上鎖,非常耗費cpu資源!如果採用pthread_cond_wait,則把線程加入等待隊列,cpu並不調度,降低cpu使用。同時,pthread_cond_wait可以保證加入等待隊列和解鎖之間是原子的

        pthread_mutex_lock(&mut_num);
        while(num == 0)
        {
			pthread_cond_wait(&cond, &qlock); //當num>0時,pthread_cond_signal(&cond)
        }
        ...
        pthread_mutex_unlock(&mut_num);

栗子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t count_lock;
pthread_cond_t count_ready;
int count;

void *decrement_count(void *arg)
{

        pthread_mutex_lock(&count_lock);
        printf("decrement:waiting %d\n",pthread_self());
        /*等待滿足條件,期間互斥量仍然可用*/
        //      while (count == 0)
        pthread_cond_wait(&count_ready, &count_lock);
        printf("decrement:count = %d,%d\n",  count,pthread_self());
        if (count == 0)
        {
                printf("exit count:%d\n",pthread_self());
                //break;
        }
        count = 0;
        pthread_mutex_unlock(&count_lock);
        
        pthread_exit(NULL);
}
void *increment_count(void *arg)
{

        pthread_mutex_lock(&count_lock);
        printf("increment:running\n");
        count = 1;
        /*通知線程條件已滿足*/
        printf("increment:count = %d\n",  count);
        pthread_cond_signal(&count_ready);
        pthread_mutex_unlock(&count_lock);

        pthread_exit(NULL);
}

int main()
{
        pthread_t tid1,tid2,tid3;
        count=0;
        pthread_mutex_init(&count_lock, NULL);
        pthread_cond_init(&count_ready, NULL);

        pthread_create(&tid1, NULL, decrement_count, NULL);
        sleep(3);
        pthread_create(&tid3, NULL, decrement_count, NULL);
        sleep(3);
        pthread_create(&tid2, NULL, increment_count, NULL);
        /*等待decrement退出*/
        pthread_join(tid2, NULL);
        printf("decrement quit\n");
        pthread_join(tid3, NULL);
        pthread_join(tid1, NULL);
        return 0;
}
decrement:waiting 1482491648
decrement:waiting 1474098944
increment:running
increment:count = 1
decrement:count = 1,1482491648
decrement quit

此時沒有驚羣,只喚醒了一個線程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章