簡體   English   中英

.net c#等待異步事件完成的最佳方法,並且代碼中仍然有“同步”流

[英].net c# best way to wait for async events to complete and still have a “synchronous” flow in your code

我的一般性問題是:如何編寫仍然清晰易懂的異步代碼,就像同步解決方案一樣?

我的經驗是,如果你需要使用像BackgroundWorker之類的東西來制作一些異步同步代碼,你就不再需要一系列易於理解的程序語句來表達你的整體意圖和活動順序,你最終會得到一堆“完成“事件處理程序,每個事件處理程序啟動下一個BackgroundWorker,生成非常難以遵循的代碼。

我知道這不是很清楚; 更具體的東西:

假設我的WinForms應用程序中的一個函數需要啟動一些amazon EC2實例,等待它們運行,然后等待它們全部接受SSH連接。 偽代碼中的同步解決方案可能如下所示:

instances StartNewInstances() {
    instances = StartInstances()
    WaitForInstancesToBecomeRunning(instances)
    WaitForInstancesToAcceptSSHConnection(instances).
    return (instances)
    }

真好。 發生的事情非常清楚,程序行動的順序非常明確。 沒有白噪聲會讓您分心,無法理解代碼和流程。 我真的想最終得到類似的代碼。

但實際上,我不能有一個同步解決方案..這些功能中的每一個都可以運行很長時間,每個功能都需要執行以下操作:更新ui,監視超時超時,並定期重試操作直到成功或超時。 簡而言之,這些中的每一個都需要在后台進行,因此前台UI線程可以繼續。

但是,如果我使用像BackgroundWorker這樣的解決方案,似乎我最終不會像上面那樣容易理解程序邏輯。 相反,我可能從我的UI線程啟動后台工作程序來執行第一個函數,然后我的ui線程在工作線程運行時返回到UI。 完成后,其“完成”事件處理程序可能會啟動下一個后台工作程序。 它完成后,其“完成”事件處理程序可能會啟動最后一個BackgroundWorker,依此類推。 這意味着您必須“完成事件處理程序的跟蹤”才能理解整個程序流程。

必須有一種更好的方法:a)讓我的UI線程能夠響應,b)讓我的異步操作能夠更新ui,最重要的是c)能夠將我的程序表達為一系列連續的步驟(就像我一樣)如上所示),以便有人可以理解結果代碼

任何和所有輸入將不勝感激! 邁克爾

我的一般性問題是:如何編寫仍然清晰易懂的異步代碼,就像同步解決方案一樣?

你等着C#5。現在不會很久。 async / await岩石。 您已經在上面的句子中描述了該功能...請參閱Visual Studio異步主頁以獲取教程,語言規范,下載等。

目前,確實沒有一種非常干凈的方式 - 這就是為什么首先需要這個功能的原因。 異步代碼很自然地變得一團糟,特別是當你考慮錯誤處理等時。

您的代碼將表示為:

async Task<List<Instance>> StartNewInstances() {
    List<Instance> instances = await StartInstancesAsync();
    await instances.ForEachAsync(x => await instance.WaitUntilRunningAsync());
    await instances.ForEachAsync(x => await instance.WaitToAcceptSSHConnectionAsync());
    return instances;
}

這是假設一些額外的工作,例如IEnumerable<T>上的擴展方法與表單

public static Task ForEachAsync<T>(this IEnumerable<T> source,
                                   Func<T, Task> taskStarter)
{
    // Stuff. It's not terribly tricky :(
}

如果Jon正確地建議你不能等待5,我建議你看一下Task Parallel Library (.NET 4的一部分)。 它為你在問題中描述的“異步執行,何時完成”這個范例提供了很多管道。 它還對異步任務本身的錯誤處理提供了堅實的支持。

Async/await真的是最好的方法。 但是,如果您不想等待,可以嘗試Continuation-passing-style或CPS。 為此,您將委托傳遞給異步方法,該方法在處理完成時調用。 在我看來,這比所有額外的事件更干凈。

這將改變此方法簽名

Foo GetFoo(Bar bar)
{
    return new Foo(bar);
}

void GetFooAsync(Bar bar, Action<Foo> callback)
{
    Foo foo = new Foo(bar);
    callback(foo);
}

然后使用它,你會有

Bar bar = new Bar();
GetFooAsync(bar, GetFooAsyncCallback);
....
public void GetFooAsyncCallback(Foo foo)
{
    //work with foo
}

GetFoo拋出異常時,這會有點棘手。 我喜歡的方法是使用GetFooAsync的簽名。

void GetFooAsync(Bar bar, Action<Func<Foo>> callback)
{
    Foo foo;
    try
    {
        foo = new Foo(bar);
    }
    catch(Exception ex)
    {
        callback(() => {throw ex;});
        return;
    }

    callback(() => foo);
}

你的回調方法將如下所示

public void GetFooAsyncCallback(Func<Foo> getFoo)
{
    try
    {
        Foo foo = getFoo();
        //work with foo
    }
    catch(Exception ex)
    {
        //handle exception
    }
}

其他方法涉及給回調兩個參數,實際結果和異常。

void GetFooAsync(Bar bar, Action<Foo, Exception> callback);

這依賴於對異常的回調檢查,這可能允許忽略它。 其他方法有兩個回調,一個用於成功,一個用於失敗。

void GetFooAsync(Bar bar, Action<Foo> callback, Action<Exception> error);

對我來說,這使得流程更加復雜,並且仍然允許忽略異常。

但是,為回調提供必須調用以獲取結果的方法會強制回調處理異常。

完成后,其“完成”事件處理程序可能會啟動下一個后台工作程序。

這是我一直在努力的事情。 基本上等待一個進程完成而不鎖定UI。

但是,您可以在一個backgroundWorker中執行所有任務,而不是使用backgroundWorker來啟動backgroundWorker。 在backgroundWorker.DoWork函數中,它在該線程上同步運行。 因此,您可以使用一個DoWork函數來處理所有3個項目。

然后你必須等待一個BackgroundWorker.Completed並擁有“更干凈”的代碼。

所以你最終可以

BackgroundWorker_DoWork
  returnValue = LongFunction1
  returnValue2 = LongFunction2(returnValue)
  LongFunction3

BackgroundWorker_ProgressReported
  Common Update UI code for any of the 3 LongFunctions

BackgroundWorker_Completed
  Notify user long process is done

在某些情況下(稍后會解釋),您可以將異步調用包裝為類似以下偽代碼的方法:

byte[] ReadTheFile() {
  var buf = new byte[1000000];
  var signal = new AutoResetEvent(false);
  proxy.BeginReadAsync(..., data => {
    data.FillBuffer(buf);
    signal.Set();
  });
  signal.WaitOne();
  return buf;
}

要使上述代碼起作用,需要從另一個線程調用回調。 所以這取決於你正在使用什么。 根據我的經驗,至少Silverlight Web服務調用是在UI線程中處理的,這意味着無法使用上述模式 - 如果UI線程被阻止,則前一個開始調用甚至無法執行。 如果您正在使用這種框架,處理多個異步調用的另一種方法是將更高級別的邏輯移動到后台線程並使用UI線程進行通信。 但是,在大多數情況下,這種方法有點過於殺戮,因為它需要一些樣板代碼來啟動和停止后台線程。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM