2010-02-25 61 views
11

我覺得我應該知道這個答案,但無論如何我會問,以防萬一我犯了一個潛在的災難性錯誤。是否安全地發出信號並立即關閉ManualResetEvent?

下面的代碼按預期執行沒有錯誤/異常:

static void Main(string[] args) 
{ 
    ManualResetEvent flag = new ManualResetEvent(false); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
     flag.WaitOne(); 
     Console.WriteLine("Work Item 1 Executed"); 
    }); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
     flag.WaitOne(); 
     Console.WriteLine("Work Item 2 Executed"); 
    }); 
    Thread.Sleep(1000); 
    flag.Set(); 
    flag.Close(); 
    Console.WriteLine("Finished"); 
} 

當然,如通常與多線程代碼的情況下,一個成功的試驗並不能證明這實際上是線程安全的。如果我在Set之前放置Close,即使文檔明確指出嘗試在Close之後執行任何操作會導致未定義的行爲,該測試也會成功。

我的問題是,當我調用ManualResetEvent.Set方法,是保證信號所有等待線程控制權返回給調用者之前?換句話說,假設我能夠保證不會有進一步的呼叫WaitOne,在這裏關閉句柄是否安全,還是有可能在某些情況下,這些代碼會阻止一些服務員收到信號或結果在ObjectDisposedException

文檔只說Set把它放在一個「信號狀態」 - 它似乎並沒有做出什麼時候服務員會真正得到該信號的任何索賠,所以我想是肯定的。

+0

一個有趣的實驗是在WaitOne()之前發生Sleep。這將允許您測試WaitOne()在'flag'爲Close()'時執行的操作。 – 2010-02-25 19:16:37

+0

@Phillip Ngan:正如文檔所述,它實際上並沒有定義。如果你在'Thread.Sleep'之前的代碼中移動'Close','WaitOne'期間會出現'ObjectDisposedException'(「Safe handle has been closed」)。另一方面,如果直接在'Thread.Sleep'後面移動'Close',兩個線程都會發出信號並且成功執行。所以,未定義的行爲,在「壞」代碼中存在爭用條件。我只想確保在我認爲是我的「好」代碼中沒有類似的未定義行爲。 :) – Aaronaught 2010-02-25 19:20:54

回答

6

當您用ManualResetEvent.Set發出信號時,可以保證所有等待該事件的線程(即在flag.WaitOne上處於阻塞狀態)都將在將控制權返回給調用者之前發出信號。

當然有,當你可以設定標誌的情況下,你的線程不會看到它,因爲它做了一些工作在檢查前旗(或nobugs如果您要創建多個線程建議):

ThreadPool.QueueUserWorkItem(s => 
{ 
    QueryDB(); 
    flag.WaitOne(); 
    Console.WriteLine("Work Item 1 Executed"); 
}); 

標誌上存在爭用,現在您可以在關閉它時創建未定義的行爲。您的標誌是您的線程之間的共享資源,您應該創建一個倒計時鎖存器,每個線程在完成時發出信號。這將消除您的flag上的爭用。

public class CountdownLatch 
{ 
    private int m_remain; 
    private EventWaitHandle m_event; 

    public CountdownLatch(int count) 
    { 
     Reset(count); 
    } 

    public void Reset(int count) 
    { 
     if (count < 0) 
      throw new ArgumentOutOfRangeException(); 
     m_remain = count; 
     m_event = new ManualResetEvent(false); 
     if (m_remain == 0) 
     { 
      m_event.Set(); 
     } 
    } 

    public void Signal() 
    { 
     // The last thread to signal also sets the event. 
     if (Interlocked.Decrement(ref m_remain) == 0) 
      m_event.Set(); 
    } 

    public void Wait() 
    { 
     m_event.WaitOne(); 
    } 
} 
  1. 上倒計時鎖存每個線程的信號。
  2. 您的主線程等待倒數鎖存器。
  3. 主線程在倒計數鎖存信號後清理。

最後,你最後睡覺的時間不是一個安全的方式來照顧你的問題,而是你應該設計你的程序,以便在多線程環境中100%安全。

UPDATE:單生產者/多消費者
這裏的假設是,你的製作人知道有多少消費者會被創建, 創建所有的消費者後,消費者在給定數量重置CountdownLatch

// In the Producer 
ManualResetEvent flag = new ManualResetEvent(false); 
CountdownLatch countdown = new CountdownLatch(0); 
int numConsumers = 0; 
while(hasMoreWork) 
{ 
    Consumer consumer = new Consumer(coutndown, flag); 
    // Create a new thread with each consumer 
    numConsumers++; 
} 
countdown.Reset(numConsumers); 
flag.Set(); 
countdown.Wait();// your producer waits for all of the consumers to finish 
flag.Close();// cleanup 
+0

這看起來可能是一個更好的概念選擇;我無法理解它是如何適用於擁有多個消費者的單一生產者的情況(這似乎適用於多個生產者,單一消費者)。你能否進一步解釋在這種情況下如何使用這個鎖存器? – Aaronaught 2010-02-25 20:01:52

+0

我已更新我的答案,以反映單個生產者/多個消費者的情況。 – Kiril 2010-02-25 20:13:42

+0

好吧,明白了。必須使用事件*和*鎖存器。不幸的是,在這種情況下,「生產者」將不知道會有多少消費者,現實比測試代碼複雜得多......但我可能會以某種方式適應這種情況。如果事後證明是最好的答案,我會接受這個。 – Aaronaught 2010-02-25 20:17:27

5

這並不好。你在這裏很幸運,因爲你只開始兩個線程。當您在雙核機器上調用Set時,它們會立即開始運行。試試這個,看它炸彈:當它是非常忙於其他任務

static void Main(string[] args) { 
     ManualResetEvent flag = new ManualResetEvent(false); 
     for (int ix = 0; ix < 10; ++ix) { 
      ThreadPool.QueueUserWorkItem(s => { 
       flag.WaitOne(); 
       Console.WriteLine("Work Item Executed"); 
      }); 
     } 
     Thread.Sleep(1000); 
     flag.Set(); 
     flag.Close(); 
     Console.WriteLine("Finished"); 
     Console.ReadLine(); 
    } 

你原來的代碼同樣會失敗的舊機器或者你目前的機器上。

+4

這是完全正確的,但它是因爲測試代碼而爆炸的,而不是因爲'Set'的行爲。在這種情況下,在一些線程開始*運行之前,事件正在關閉。將睡眠超時增加到5秒,可以在四核上運行。我可以通過序列化對事件的訪問來阻止上述情況,並在關閉後立即將其置零;我主要關心已經在等待句柄上等待的線程。 – Aaronaught 2010-02-25 19:56:35

+0

任何長時間的睡眠後,線程都無法保證實際開始運行。長時間睡眠只會降低ObjectDisposedException的風險,但不能消除它。這種時序依賴性是任何人對線程做出假設的棺材中的終極釘子。 – 2010-02-25 20:16:20

+0

當然我不會在生產代碼中使用'Thread.Sleep' - 實際的實現使用了各種同步原語。但那確實是一個微妙的競爭條件,現在已經被修復了。感謝您的輸入; +1是第一個打電話給我的人。 – Aaronaught 2010-02-25 22:35:41

0

我的意思是有一個競爭條件。具有基於條件變量書面事件對象,你會得到這樣的代碼:

mutex.lock(); 
while (!signalled) 
    condition_variable.wait(mutex); 
mutex.unlock(); 

因此,雖然該事件可能是信號,等待事件的代碼可能仍然需要訪問該活動的組成部分。

根據Close的文檔,這隻會釋放非託管資源。所以如果這個活動只使用託管資源,你可能會很幸運。但是,這可能會在未來發生變化,所以我會在預防措施方面犯錯,直到您知道它不再被使用爲止。

0

看起來像一個危險的模式對我來說,即使因(當前)實現,它是確定。您正在嘗試處理可能仍在使用的資源。

它就像新建和構建一個對象,並在該對象的消費者完成之前一味地刪除它。

甚至在這裏也有一個problem。即使在其他線程有機會運行之前,程序可能會退出。線程池線程是後臺線程。

鑑於您必須等待其他線程,您可能還需要清理之後。