9

我的一般性问题是:您如何编写仍然清晰且易于遵循的异步代码,就像同步解决方案一样?

我的经验是,如果你需要使一些同步代码异步,使用像 BackgroundWorker 这样的东西,你不再有一系列易于遵循的程序语句来表达你的整体意图和活动顺序,你最终会得到一堆“完成”事件处理程序,每个处理程序都启动下一个 BackgroundWorker,生成的代码非常难以遵循。

我知道这不是很清楚。更具体的东西:

假设我的 WinForms 应用程序中的一个函数需要启动一些亚马逊 EC2 实例,等待它们开始运行,然后等待它们全部接受 SSH 连接。伪代码中的同步解决方案可能如下所示:

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

那很好。发生的事情很清楚,程序动作的顺序也很清楚。没有白噪音分散您对代码和流程的理解。我真的很想得到看起来像这样的代码。

但实际上,我不能有一个同步的解决方案..这些功能中的每一个都可以运行很长时间,并且每个都需要执行以下操作:更新 ui,监控超时,并定期重试操作,直到成功或超时。简而言之,这些都需要在后台发生,以便前台 UI 线程可以继续运行。

但是,如果我使用像 BackgroundWorker 这样的解决方案,似乎我最终不会像上面那样轻松地遵循程序逻辑。相反,我可能会从我的 UI 线程启动一个后台工作程序来执行第一个功能,然后我的 ui 线程在工作线程运行时返回到 UI。当它完成时,它的“完成”事件处理程序可能会启动下一个后台工作程序。当它完成时,它的“完成”事件处理程序可能会启动最后一个 BackgroundWorker,依此类推。这意味着您必须“跟踪”完成事件处理程序才能了解整个程序流程。

必须有一种更好的方法a)让我的UI线程响应,b)让我的异步操作能够更新ui,最重要的是c)能够将我的程序表达为一系列连续的步骤(就像我已经如上所示)以便有人可以理解生成的代码

任何和所有输入将不胜感激!迈克尔

4

5 回答 5

11

我的一般性问题是:您如何编写仍然清晰且易于遵循的异步代码,就像同步解决方案一样?

你等待 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 :(
}
于 2012-07-26T17:15:08.883 回答
3

如果您不能像 Jon 正确建议的那样等待 5,我建议您查看Task Parallel Library(.NET 4 的一部分)。它围绕您在问题中描述的“异步执行此操作,并在它完成时执行此操作”范例提供了很多管道。它还对异步任务本身的错误处理提供了坚实的支持。

于 2012-07-26T17:20:05.183 回答
3

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

对我来说,这使流程更加复杂,并且仍然允许忽略异常。

但是,为回调提供一个必须调用才能获得结果的方法会强制回调处理异常。

于 2012-07-26T17:35:01.150 回答
1

当它完成时,它的“完成”事件处理程序可能会启动下一个后台工作程序。

这是我一直在努力解决的问题。基本上等待进程完成而不锁定 UI。

但是,您可以在一个 backgroundWorker 中完成所有任务,而不是使用 backgroundWorker 来启动 backgroundWorker。在 backgroundWorker.DoWork 函数中,它在该线程上同步运行。因此,您可以拥有一个处理所有 3 个项目的 DoWork 函数。

然后你必须等待一个 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
于 2012-07-26T17:25:33.950 回答
0

在某些情况下(稍后将解释),您可以将异步调用包装到类似以下伪代码的方法中:

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 线程被阻塞,甚至无法执行之前的 begin 调用。如果您正在使用这种框架,处理多个异步调用的另一种方法是将您的更高级别的逻辑移动到后台线程并使用 UI 线程进行通信。然而,在大多数情况下,这种方法有点过头了,因为它需要一些样板代码来启动和停止后台线程。

于 2012-07-26T18:12:26.727 回答