條件變量虛假喚醒

一、使用信號量與鎖配合也可以實現多線程操作,使用條件變量有什麼優勢?

這個問題感覺也沒明白,看了stack overflow上的一些討論,感覺條件變量能夠實現的,信號量都能實現。

  1. 使用條件變量可以一次喚醒所有等待者,而這個信號量沒有的功能,感覺是最大區別。
  2. 信號量是有一個值(狀態的),而條件變量是沒有的,沒有地方記錄喚醒(發送信號)過多少次,也沒有地方記錄喚醒線程(wait返回)過多少次。從實現上來說一個信號量可以是用mutex + counter + condition variable實現的,而一個condition感覺沒特別的地方,只有有個“原子操作”的普通event。因爲信號量有一個狀態,如果想精準的同步,那麼信號量可能會有特殊的地方。
  3. 其他說的說法就有些爭議。
    有的說linux下信號量多在內核空間,而條件變量多在用戶空間,而條件變量多在用戶空間實現。信號量在內核空間的話會會有過多的用戶空間/內核空間切換,消耗性能。
    下面一段引用的話:

在Posix.1基本原理一文聲稱,有了互斥鎖和條件變量還提供信號量的原因是:“本標準提供信號量的而主要目的是提供一種進程間同步的方式;這些進程可能共享也可能不共享內存區。互斥鎖和條件變量是作爲線程間的同步機制說明的;這些線程總是共享(某個)內存區。這兩者都是已廣泛使用了多年的同步方式。每組原語都特別適合於特定的問題”。儘管信號量的意圖在於進程間同步,互斥鎖和條件變量的意圖在於線程間同步,但是信號量也可用於線程間,互斥鎖和條件變量也可用於進程間。應當根據實際的情況進行決定。信號量最有用的場景是用以指明可用資源的數量。
由於起源不同,導致了兩種理念,一中理念力挺條件變量(condition variable),覺得信號量沒有什麼用(例如POSIX
Thread模型中沒有信號量的概念,雖然也提出了Posix
Semaphore,但是爲什麼一開始不把它放在一起呢?);另一理念恰好相反(例如window剛開始沒有條件變量的概念,只有信號量的概念)。
進化到後來,目前的linux和window都同時具備了這二者。

二、虛擬喚醒問題

條件變量中存在的問題:虛假喚醒
Linux中幫助中提到的:

在多核處理器下,pthread_cond_signal可能會激活多於一個線程(阻塞在條件變量上的線程)。 On a
multi-processor, it may be impossible for an implementation of
pthread_cond_signal() to avoid the unblocking of more than one thread
blocked on a condition variable.
結果是,當一個線程調用pthread_cond_signal()後,多個調用pthread_cond_wait()或pthread_cond_timedwait()的線程返回。這種效應成爲”虛假喚醒”(spurious
wakeup)

The effect is that more than one thread can return from its call to
pthread_cond_wait() or pthread_cond_timedwait() as a result of one
call to pthread_cond_signal(). This effect is called “spurious
wakeup”. Note that the situation is self-correcting in that the number
of threads that are so awakened is finite; for example, the next
thread to call pthread_cond_wait() after the sequence of events above
blocks.

雖然虛假喚醒在pthread_cond_wait函數中可以解決,爲了發生概率很低的情況而降低邊緣條件(fringe
condition)效率是不值得的,糾正這個問題會降低對所有基於它的所有更高級的同步操作的併發度。所以pthread_cond_wait的實現上沒有去解決它。

While this problem could be resolved, the loss of efficiency for a
fringe condition that occurs only rarely is unacceptable, especially
given that one has to check the predicate associated with a condition
variable anyway. Correcting this problem would unnecessarily reduce
the degree of concurrency in this basic building block for all
higher-level synchronization operations.

所以通常的標準解決辦法是這樣的:
將條件的判斷從if 改爲while,舉例子說明:
下面爲線程處理函數:

static void *thread_func(void *arg)
{
    while (1) {
    pthread_mutex_lock(&mtx);           //這個mutex主要是用來保證pthread_cond_wait的併發性
    while (msg_list.empty())   {     //pthread_cond_wait裏的線程可能會被意外喚醒(虛假喚醒),如果這個時候,則不是我們想要的情況。這個時候,應該讓線程繼續進入pthread_cond_wait
        pthread_cond_wait(&cond, &mtx);
    }
        msg = msg_list.pop();
        pthread_mutex_unlock(&mtx);             //臨界區數據操作完畢,釋放互斥鎖
        // handle msg
    }
    return 0;
}

如果不存在虛假喚醒的情況,那麼下面代碼:

    while (msg_list.empty())   {
        pthread_cond_wait(&cond, &mtx);
    }

可以爲

    if (msg_list.empty())   {
        pthread_cond_wait(&cond, &mtx);
    }

但是存在虛假喚醒的時候,如果用if,而不用while,那麼但被虛假喚醒的時候,不會再次while判斷,而是繼續下面執行msg = msg_list.pop();這其實是邏輯上有問題的。因爲下面的代碼已經假定了msg_list不是空的。寫成如下也是可以的:

    if (msg_list.empty())   {
        pthread_cond_wait(&cond, &mtx);
    }
    if(msg_list.empty())
        continue;

pthread_cond_wait中的while()不僅僅在等待條件變量前檢查條件變量,實際上在等待條件變量後也檢查條件變量。

這樣對condition進行多做一次判斷,即可避免“虛假喚醒”.
這就是爲什麼在pthread_cond_wait()前要加一個while循環來判斷條件是否爲假的原因。
有意思的是這個問題也存在幾乎所有地方,包括: linux 條件等待的描述, POSIX Threads的描述, window API(condition variable), java等等。

在linux的幫助中對條件變量的描述是:

添加while檢查的做法被認爲是增加了程序的健壯性,在IEEE Std 1003.1-2001中認爲spurious wakeup是允許的。

An added benefit of allowing spurious wakeups is that applications are
forced to code a predicate-testing-loop around the condition wait.
This also makes the application tolerate superfluous condition
broadcasts or signals on the same condition variable that may be coded
in some other part of the application. The resulting applications are
thus more robust. Therefore, IEEE Std 1003.1-2001 explicitly documents
that spurious wakeups may occur.

在POSIX Threads中:
David R. Butenhof 認爲多核系統中 條件競爭(race condition)導致了虛假喚醒的發生,並且認爲完全消除虛假喚醒本質上會降低了條件變量的操作性能。

“…, but on some multiprocessor systems, making condition wakeup
completely predictable might substantially slow all condition variable
operations. The race conditions that cause spurious wakeups should be
considered rare”

在window的條件變量中:

MSDN幫助中描述爲,spurious wakeups問題依然存在,條件需要重複check。

Condition variables are subject to spurious wakeups (those not
associated with an explicit wake) and stolen wakeups (another thread
manages to run before the woken thread). Therefore, you should recheck
a predicate (typically in a while loop) after a sleep operation
returns.

在Java中,對等待的寫法如下:

synchronized (obj) {  
    while (<condition does not hold>)  
        obj.wait(); 
     ... // Perform action appropriate to condition  

 }

Effective java 曾經提到Item 50: Never invoke wait outside a loop.
顯然,虛假喚醒是個問題,但它也是在JLS的第三版的JDK5的修訂中才得以澄清。在JDK 5的Javadoc進行更新

A thread can also wake up without being notified, interrupted, or
timing out, a so-called spurious wakeup. While this will rarely occur
in practice, applications must guard against it by testing for the
condition that should have caused the thread to be awakened, and
continuing to wait if the condition is not satisfied. In other words,
waits should always occur in loops. Apparently, the spurious wakeup
is an issue (I doubt that it is a well known issue) that intermediate
to expert developers know it can happen but it just has been clarified
in JLS third edition which has been revised as part of JDK 5
development. The javadoc of wait method in JDK 5 has also been updated

三、pthread_cond_timedwait有幾個原子操作?

幫助文檔中說了了一個:

These functions atomically release mutex and cause the calling thread
to block on the condition variable cond;

這個必須是原子操作,因爲條件變量與信號量不同,如果不是原子操作,在釋放鎖後,等待信號前,如果正好發生了信號,這次信號就永久丟失了,沒有信號量中的“狀態”表示“曾經有一個信號發生過”。

幫助文檔中還有一句“Upon successful return, the mutex has been locked and is owned by the calling thread. ”。返回的時候是不是原子操作影響不大,但是返回的時候必須是獲取到鎖的。即使發生了上面說的虛擬喚醒,那麼他們也是一個個獲取到鎖之後返回的。因爲如果是獲取不到鎖也返回,那麼對條件變量的判斷就是不安全的了。

參考:

http://blog.csdn.net/fengge8ylf/article/details/6896380

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