【问题标题】:Async/Await for sequential I/O bound tasks [duplicate]异步/等待顺序 I/O 绑定任务 [重复]
【发布时间】:2020-03-24 17:38:18
【问题描述】:

我正在开发固件更新应用程序,并尝试使用 Async/Await 来避免阻塞 UI。我面临的问题是更新固件的过程需要按顺序进行一系列步骤(这不是异步的)。然而,当涉及 UI 以避免冻结应用程序时,看起来异步仍然是要走的路。那么我们如何才能不阻塞 UI,顺序完成一系列长时间运行的任务,又不吃不饱也不浪费线程呢?

这里有一个非异步的例子来说明这个问题:

public class FirmwareUpdateViewModel : ObservableObject
{
   //ctor
   public FirmwareUpdateViewModel()
   {
       // Process should start on page load
       updateFirmware();
   }

   private void updateFirmware()
   {
       //...

       DeviceHandshake();

       SendMessage(Commands.RestartDevice);

       GetFWFileData();

       WaitForDevice(timeout);

       SendMessage(PreliminaryData);

       //...
   }
}

我的第一次尝试是await Task.Run(() =>一切:

public class FirmwareUpdateViewModel : ObservableObject
{
   //ctor
   public FirmwareUpdateViewModel()
   {
       updateFirmwareAsync();
   }

   private async void updateFirmwareAsync()
   {
       //...

       await Task.Run(() => DeviceHandshakeAsync());

       await Task.Run(() => SendMessageAsync(Commands.RestartDevice));

       //...
   }
}

除了async void 之外,这里的问题是由于异步的病毒性质,当我在DeviceHandshakeAsync 中等待时,加上如果Task.Run() 在它自己的线程上运行,那么我们就浪费了大量的线程。

接下来我尝试删除 Task.Run(() => 并等待每个方法,但据我所知,当子方法遇到内部 await 并开始在前一个之前运行下一个子方法时,它们将返回完成。现在我正在尝试异步运行firmwareUpdate方法,但不等待子方法以保持它们同步并对抗病毒性质,但这会阻塞UI:

public class FirmwareUpdateViewModel : ObservableObject
{
   //ctor
   public FirmwareUpdateViewModel()
   {
       Result = new NotifyTaskComplete<bool>(updateFirmwareAsync());
   }

   private async Task<bool> updateFirmwareAsync()
   {
       //...

       DeviceHandshake();

       SendMessage(Commands.RestartDevice);

       //...
   }
}

这里的问题显然是没有awaits,我们在UI线程上同步运行。对于我来说,在这里理解正确的方法还没有“点击”。我们如何正确地做到这一点?

编辑:我意识到我遗漏了大部分问题...... USB 由同步的第 3 方库处理。所以我需要以某种方式包装这些函数以避免阻塞 UI:

public class FirmwareUpdateViewModel : ObservableObject
{
   //ctor
   public FirmwareUpdateViewModel()
   {
       // Don't block the UI with this
       updateFirmware();
   }

   private void updateFirmware()
   {
       //...
       // Run all non-blocking but sequentially
       DeviceHandshake();

       SendMessage(Commands.RestartDevice);

       GetFWFileData();

       WaitForDevice(timeout);

       SendMessage(PreliminaryData);

       //...
   }

   private void SendMessage(Command)
   {
      // Existing code, can't make async
      USBLibrary.Write(Command);

   }
}

这就是我如此困惑的原因,因为 async 想要传播但我不能在构造函数和结束库之间。

【问题讨论】:

  • wasting a ton of threads不,会在线程池上执行,线程池是固定的一组线程
  • “据我所知,子方法会在遇到内部等待时返回并开始运行下一个子方法” - 没有。这就是你误会的核心。任何对 async/await 的基本介绍都应该清楚这一点。
  • @Renat 仍然 I/O 绑定的工作不应该包装在 CPU 绑定的任务中,只是为了使其异步。第一个选择应该是使 I/O 本身异步,例如通过通过 TaskCompletionSource 将 I/O 完成事件或完成回调包装到 I/O 绑定任务中。
  • 这能回答你的问题吗? Perform Multiple Async Method Calls Sequentially
  • 是的,基本上就是这样。您在方法中创建一个 TaskCompletionSource tcs。你在方法中调用的 I/O 方法必须有某种完成事件或回调,当这个事件被触发时,你调用tcs.TrySetResult。你的方法只返回tcs.Task。结果是您的方法仅启动 I/O 然后退出,当您等待您的方法时,在调用 TrySetResult 后,等待之后的所有内容都将异步执行。

标签: c# multithreading asynchronous async-await


【解决方案1】:

关于浪费了多少线程。这段代码:

private async void updateFirmwareAsync()
{
    //...
    await Task.Run(() => DeviceHandshakeAsync());
    await Task.Run(() => SendMessageAsync(Commands.RestartDevice));
    //...
}

...很可能浪费了几乎零线程。假设DeviceHandshakeAsyncSendMessageAsync方法是异步的,也就是说假设它们返回一个Task,那么ThreadPool线程将只用于调用这些方法并获取任务,然后立即释放回到线程池,因为线程没有其他事情要做。现在,一个表现良好的异步方法在调用时不会阻塞调用者,并且几乎立即返回一个不完整的Task。因此,除非这些方法表现不佳,否则ThreadPool 线程将在以微秒为单位的时间跨度内使用。

但是如果这两种方法是同步的呢?

private async void updateFirmwareAsync()
{
    //...
    await Task.Run(() => DeviceHandshake());
    await Task.Run(() => SendMessage(Commands.RestartDevice));
    //...
}

这段代码浪费了一个ThreadPool 线程。可能是同一个线程,或者可能是按顺序使用的两个不同线程。这个线程已经被创建(除非你的代码在那之前从未使用过ThreadPool),所以说线程被浪费了是夸大其词。更准确地说,在固件更新的整个过程中,将保留一个 ThreadPool 线程,因此无法进行其他工作。如果你的程序确实大量使用了ThreadPool线程,导致ThreadPoolstarvation引起的打嗝,你可以在应用初始化时通过调用ThreadPool.SetMinThreads方法抢先增加线程池。

顺便说一句,here 是我关于为什么在 UI 应用程序的事件处理程序中抢先使用 await Task.Run 是个好主意的论点。

【讨论】:

  • 谢谢你。我的部分困惑是我无法想象这一切是如何像使用同步代码一样组合在一起的。例如,DeviceHandshake() 应该在两个设备都提升 HS 引脚时几乎立即出现。 SendMessage() 可以更长,因为它们通过 i2c 发送字节。但实际的固件更新方法是一个 while 循环,它必须从文件中读取 256 个字节,计算校验和,创建数据包并发送它,然后等待设备再次表示准备就绪。我以为我读到了单个线程被整个方法阻塞是不好的。
  • 其中的任何方法都可能在内部使用多个线程。如果不研究它们的源代码,或者使用工具来监视当前进程使用的线程,您将无法确定。无论他们做什么,您都无法控制或以任何方式改变他们的工作方式。通过我的回答,我想明确指出,使用 await Task.Run 的开销可以忽略不计,并且不会导致比不使用它的替代方案更多的线程被阻塞。实际上我提倡在 UI 应用程序的异步处理程序中抢先使用这种方法,以确保 UI 将保持响应。
【解决方案2】:

我认为有两种方法可以解决这个问题。

1:

private async void updateFirmwareAsync()
{
   await Task.Run(() => 
   { 
       DeviceHandshakeAsync());
       SendMessage(Commands.RestartDevice);
       GetFWFileData();
       WaitForDevice(timeout);
       SendMessage(PreliminaryData);
   });
}

所以在单个任务中按顺序运行它们,或者

2:

private async void updateFirmwareAsync()
{
   await Task.Run(() => DeviceHandshakeAsync())
             .ContinueWith(() => SendMessage(Commands.RestartDevice))
             .ContinueWith(() => GetFWFileData())
             .ContinueWith(() => WaitForDevice(timeout))
             .ContinueWith(() => SendMessage(PreliminaryData))
   });
}

在实践中,它们的实现大致相同,尽管 (1) 保证它们都在同一个线程中运行,而 (2) 意味着它们可能在不同的线程中运行,所以更像你最初的多个 await 想法。

【讨论】:

  • 虽然这些都是不得已而为之的方法。第一选择是使 I/O 方法真正异步,例如通过利用 I/O 完成事件或回调并通过 TaskCompletionSource 将它们转换为 I/O 绑定任务。通过 Task.Run 浪费线程池线程应该是最后的选择。
  • 你的第一个建议很好,但第二个真的很糟糕。起初将awaitContinueWith 混合并不是一个好主意。这两种实现延续的方式的行为略有不同。您应该选择其中一个,最好是await。第二个也是更大的问题是吞下异常并继续下一步,即使前一个失败了。很可能这不是 OP 想要的。
【解决方案3】:

你应该链接函数 ConfigureAwait:

 public async void FirmwareUpdateViewModel() {
     await updateFirmwareAsync().ConfigureAwait(false);
 }

这将让您异步调用任务而无需等待它完成,这会导致用户界面停止绘制。

【讨论】:

  • 需要一个async 方法在其中调用await
猜你喜欢
  • 1970-01-01
  • 2014-03-18
  • 2014-09-06
  • 2013-02-10
  • 2017-07-22
  • 1970-01-01
  • 1970-01-01
  • 2016-07-13
  • 1970-01-01
相关资源
最近更新 更多