2011-10-27 122 views
2

我有這兩個類:LineWriter和AsyncLineWriter。我的目標是允許調用者在AsyncLineWriter的方法上排隊多個掛起的異步調用,但在封面下永遠不會允許它們並行運行;即他們必須以某種方式排隊等待彼此。而已。這個問題的其餘部分給出了一個完整的例子,所以對於我真正問的問題絕對沒有歧義。如何強制C#異步操作以...同步方式運行?

LineWriter具有大約需要5秒鐘運行單個同步方法(的WriteLine):

public class LineWriter 
{ 
    public void WriteLine(string line) 
    { 
      Console.WriteLine("1:" + line); 
      Thread.Sleep(1000); 
      Console.WriteLine("2:" + line); 
      Thread.Sleep(1000); 
      Console.WriteLine("3:" + line); 
      Thread.Sleep(1000); 
      Console.WriteLine("4:" + line); 
      Thread.Sleep(1000); 
      Console.WriteLine("5:" + line); 
      Thread.Sleep(1000); 
    } 
} 

AsyncLineWriter只是封裝LineWriter並提供一個異步接口(BeginWriteLine和EndWriteLine):

public class AsyncLineWriter 
{ 
    public AsyncLineWriter() 
    { 
     // Async Stuff 
     m_LineWriter = new LineWriter(); 
     DoWriteLine = new WriteLineDelegate(m_LineWriter.WriteLine); 
     // Locking Stuff 
     m_Lock = new object(); 
    } 

#region Async Stuff 

    private LineWriter m_LineWriter; 
    private delegate void WriteLineDelegate(string line); 
    private WriteLineDelegate DoWriteLine; 
    public IAsyncResult BeginWriteLine(string line, AsyncCallback callback, object state) 
    { 
     EnterLock(); 
     return DoWriteLine.BeginInvoke(line, callback, state); 
    } 
    public void EndWriteLine(IAsyncResult result) 
    { 
     DoWriteLine.EndInvoke(result); 
     ExitLock(); 
    } 

#endregion 

#region Locking Stuff 

    private object m_Lock; 
    private void EnterLock() 
    { 
     Monitor.Enter(m_Lock); 
     Console.WriteLine("----EnterLock----"); 
    } 
    private void ExitLock() 
    { 
     Console.WriteLine("----ExitLock----"); 
     Monitor.Exit(m_Lock); 
    } 

#endregion 

} 

正如我在第一段中所說的,我的目標是一次只允許一個掛起的異步操作。我會真的喜歡它,如果我不需要在這裏使用鎖;也就是說,如果BeginWriteLine可以返回一個總是立即返回並保持預期行爲的IAsyncResult句柄,那將會很棒,但我不知道該怎麼做。所以下一個最好的方式來解釋我後來似乎只是使用鎖。

不過,我的「鎖定東西」部分沒有按預期工作。我期望上面的代碼(在異步操作開始之前運行EnterLock,在異步操作結束之後運行ExitLock)只允許在任何給定時間運行一個掛起的異步操作。

如果我運行下面的代碼:

static void Main(string[] args) 
{ 
    AsyncLineWriter writer = new AsyncLineWriter(); 

    var aresult = writer.BeginWriteLine("atest", null, null); 
    var bresult = writer.BeginWriteLine("btest", null, null); 
    var cresult = writer.BeginWriteLine("ctest", null, null); 

    writer.EndWriteLine(aresult); 
    writer.EndWriteLine(bresult); 
    writer.EndWriteLine(cresult); 

    Console.WriteLine("----Done----"); 
    Console.ReadLine(); 
} 

預計看到下面的輸出:

----EnterLock---- 
1:atest 
2:atest 
3:atest 
4:atest 
5:atest 
----ExitLock---- 
----EnterLock---- 
1:btest 
2:btest 
3:btest 
4:btest 
5:btest 
----ExitLock---- 
----EnterLock---- 
1:ctest 
2:ctest 
3:ctest 
4:ctest 
5:ctest 
----ExitLock---- 
----Done---- 

而是我看到以下,這意味着他們都只是平行運行,好像鎖不起作用:

----EnterLock---- 
----EnterLock---- 
----EnterLock---- 
1:atest 
1:btest 
1:ctest 
2:atest 
2:btest 
2:ctest 
3:atest 
3:btest 
3:ctest 
4:atest 
4:btest 
4:ctest 
5:atest 
5:btest 
5:ctest 
----ExitLock---- 
----ExitLock---- 
----ExitLock---- 
----Done---- 

我假設鎖被「忽略」,因爲(並糾正我,如果我錯了)我將它們從同一個線程全部鎖定。我的問題是:如何可以我得到了我的預期行爲?而「不要使用異步操作」不是一個可以接受的答案。

有一個更大,更復雜的真實生活用例,使用異步操作,有時不可能有多個真正的並行操作,但我需要至少模擬行爲,即使它意味着排隊操作並一個接一個地運行它們。具體而言,AsyncLineWriter的接口不應該改變,但它的內部行爲需要以線程安全的方式排隊它的任何異步操作。另一個問題是,我無法將鎖添加到LineWriter的WriteLine中,因爲在我的實際情況中,這是一種我無法更改的方法(儘管在此示例中實際上確實會給我預期的輸出)。

設計用於解決類似問題的代碼的鏈接可能足以讓我走上正確的道路。或者也許有一些替代想法。謝謝。

P.S.如果你想知道什麼樣的用例可能會使用這樣的事情:它是一個維護網絡連接的類,一次只能激活一個操作;我爲每個操作使用異步調用。沒有明顯的方法可以通過單一網絡連接實現真正的並行通信線路,因此它們需要以某種方式彼此等待。

+0

我說'預計'我應該說'想要'。 –

回答

4

鎖不可能在這裏工作。您試圖在一個線程(調用線程)上輸入鎖,並在另一個線程(ThreadPool)上退出它。
由於.Net鎖是可重入的,因此第二次在主叫線程上輸入它不會等待。 (它已經等了,它會死鎖,因爲你只能退出該線程的鎖)

相反,你應該創建一個方法來調用鎖中的同步版本,然後異步調用該方法。
這樣,你在異步線程而不是調用者線程上輸入鎖,並在方法結束後在同一線程上退出它。


但是,這是效率低下,因爲你浪費線程等待鎖。
最好是讓單個線程有一個委託隊列在其上執行,以便調用異步方法將啓動該線程的一個副本,或者在線程已經運行的情況下將其添加到隊列中。
當此線程完成隊列時,它將退出,然後在下次調用異步方法時重新啓動。 我已經在類似的情況下做了這個;見my earlier question

請注意,您需要自己實現IAsyncResult,或使用回調模式(通常更簡單)。

+0

好吧,我確實說過「我不能在同步版本中加鎖」,因爲我無法編輯那些代碼「,但是你說」把鎖放入新代碼中,然後調用內部的同步版本「?呃......不知道爲什麼我沒有想到這一點。而且我也不知道你如何快速閱讀我的整個問題,並在我發佈後的30秒內回答,這非常了不起。謝謝。 –

+0

我已經處理過類似的事情。如果你想採取我的方法,請閱讀[這些評論](http://stackoverflow.com/questions/6456571/how-can-i-make-sure-that-exactly-one-thread-will-do-something/ 6456603#6456603)。 – SLaks

+0

我認爲在我們的使用案例中,即使效率低下,它也會給出正確的行爲,而且這種情況非常罕見,這就是我們真正關心的。如果我們發現它更頻繁發生,我可能會嘗試稍後使用隊列。 –

0

我創建了下面的 「Asynchronizer」 類:

public class Asynchronizer 
{ 
    public readonly Action<Action> DoAction; 

    public Asynchronizer() 
    { 
     m_Lock = new object(); 
     DoAction = new Action<Action>(ActionWrapper); 
    } 
    private object m_Lock; 
    private void ActionWrapper(Action action) 
    { 
     lock (m_Lock) 
     { 
      action(); 
     } 
    } 
} 

您可以使用它像這樣:

public IAsyncResult BeginWriteLine(string line, AsyncCallback callback, object state) 
{ 
    Action action =() => m_LineWriter.WriteLine(line); 
    return m_Asynchronizer.DoAction.BeginInvoke(action, callback, state); 
} 
public void EndWriteLine(IAsyncResult result) 
{ 
    m_Asynchronizer.DoAction.EndInvoke(result); 
} 

這種方式很容易,即使同步多個異步操作他們有不同的方法簽名。例如,我們也可以這樣做:

public IAsyncResult BeginWriteLine2(string line1, string line2, AsyncCallback callback, object state) 
{ 
    Action action =() => m_LineWriter.WriteLine(line1, line2); 
    return m_Asynchronizer.DoAction.BeginInvoke(action, callback, state); 
} 
public void EndWriteLine2(IAsyncResult result) 
{ 
    m_Asynchronizer.DoAction.EndInvoke(result); 
} 

用戶可以排隊儘可能多的BeginWriteLine/EndWriteLine和BeginWriteLine2/EndWriteLine2因爲他們想要的,他們將在交錯的,同步的方式調用(雖然你將有許多線程打開,因爲操作正在等待處理,所以只有在您知道您一次只能有一個異步操作掛起時纔可行)。像SLak指出的那樣,更好,更復雜的解決方案是使用專用的隊列線程並將動作排入其中。

+1

順便說一下,你可以用'Action '替換你的委託類型。 – SLaks

+1

實際上,您可以放棄我的解決方案,而無需通過用更復雜的無鎖類替換「Asynchronizer」來修改代碼的其餘部分。 – SLaks

+0

酷!這很好聽,因爲Asynchronizer(儘管天真)正在通過它的測試。下一步是嘗試你的替代品。 –