【问题标题】:async await WhenAll not waitingasync await WhenAll 不等待
【发布时间】:2016-03-08 05:33:11
【问题描述】:

我正在 VS2013、.NET FW 4.5.1 中开发 WinForms 应用程序。这是我关于结构的内联 cmets 的简化代码:

// Progress object implementing IProgress<MyProgressData>
var progressCallback = new Progress<MyProgressData>();

// listOfMyList is actually List<List<MyObject>>, which contains list of
// list of MyObject's which will be executed as tasks at once.
// For example, this would be sample structure for list of lists:
// List1
//   MyObject1
//   MyObject2
//   MyObject3
// List2
//   MyObject4
//   MyObject5
//   MyObject6
// List1's and List2's objects would be executed as all tasks at once, but List1 and List2 respectively
// would be executed one after another (because of resources usage inside TASK CODE)
foreach (var myItem in listOfMyList)
{
  var myList = myItem.ToList();

  // Create a list of tasks to be executed (20 by default; each taking from 30-60 seconds)
  // Here cs is actually MyObject
  var myTasks = myList.Select(cs => Task.Run(async () =>
  {
    // TASK CODE (using cs as an input object and using "cancellationToken.ThrowIfCancellationRequested();" inside execution to cancel executing if requested)
  }, cancellationToken));

  await Task.WhenAll(myTasks); // Wait for all tasks to finish

  // Report progress to main form (this actually calls an event on my form)
  await Task.Run(() => progressCallback.Report(new MyProgressData() { props }), CancellationToken.None);
}

如您所见,我构造了进度对象,然后我有了列表列表。顶级列表中的每个项目都应该以序列化的方式执行(一个接一个)。每个项目的列表元素都应该以任务的形式一次执行。 到目前为止一切顺利,所有任务都开始了,甚至 WhenAll 等待它们。或者至少我是这么认为的。我已将日志记录放在相关方法中,以向我展示代码执行情况。事实证明,当进度逻辑(在底部)正在执行时,foreach 循环开始执行另一批任务,这是不应该的。 我在这里错过了什么吗?进度代码是否不会阻塞或等待 Report 方法完成执行。也许我错过了关于异步/等待的东西。使用 await 我们确保代码在方法完成之前不会继续?它不会阻塞当前线程,但它也不会继续执行? 我的 foreach 循环是否有可能(因为它发生了,它可能是)在进度报告仍在进行时继续执行?

此代码位于异步方法中。它实际上是这样调用的(假设这个方法是async MyProblematicMethod()):

while (true)
{
    var result = await MyProblematicMethod();
    if (result.HasToExitWhile)
        break;
}

从 MyProblematicMethod 开始的每个方法都使用 await 来等待异步方法,并且不会被多次调用。

【问题讨论】:

  • 无法在此处重现您的问题。在任务完成之前,执行不会传递任何await 运算符。我猜你的错误在更远的地方。您的方法是否被多次调用?
  • 已编辑问题以显示其调用方式。它一直是异步的,到处都是等待。有可能,我在其他地方有错误,现在快一周都找不到了。
  • 这应该可以。发布任务代码。也许 lambda 主体会返回。
  • 深入挖掘后,我猜您的问题可能与progressCallback.Report() 有关。此方法绝对不会等待事件处理程序完成,因为看起来处理程序在线程池上运行。这意味着您的进度事件处理程序可能会与循环的下一次迭代一起执行,这听起来就像您所描述的那样。
  • 看起来它确实在 UI 应用程序的主线程上运行,这是有道理的。在控制台程序上进行测试。无论如何,在这种情况下,您的 async 方法正在线程池线程上运行。这是同样的想法。正在等待调用 Report() 的 Task,但 Report() 调用正在调用另一个线程上的事件处理程序并返回。事件处理程序仍将与您的下一次循环迭代一起运行。

标签: c# .net winforms async-await


【解决方案1】:

根据 Glorin 的建议,即 IProgress.Report 在触发事件处理程序后立即返回,我创建了 Progress 类的精确副本,它使用同步上下文.Send 而不是 Post:

public sealed class ProgressEx<T> : IProgress<T>
{
    private readonly SynchronizationContext _synchronizationContext;
    private readonly Action<T> _handler;
    private readonly SendOrPostCallback _invokeHandlers;

    public event EventHandler<T> ProgressChanged;

    public ProgressEx(SynchronizationContext syncContext)
    {
        // From Progress.cs
        //_synchronizationContext = SynchronizationContext.CurrentNoFlow ?? ProgressStatics.DefaultContext;
        _synchronizationContext = syncContext;
        _invokeHandlers = new SendOrPostCallback(InvokeHandlers);
    }

    public ProgressEx(SynchronizationContext syncContext, Action<T> handler)
        : this(syncContext)
    {
        if (handler == null)
            throw new ArgumentNullException("handler");
        _handler = handler;
    }

    private void OnReport(T value)
    {
        // ISSUE: reference to a compiler-generated field
        if (_handler == null && ProgressChanged == null)
            return;
        _synchronizationContext.Send(_invokeHandlers, (object)value);
    }

    void IProgress<T>.Report(T value)
    {
        OnReport(value);
    }

    private void InvokeHandlers(object state)
    {
        T e = (T)state;
        Action<T> action = _handler;
        // ISSUE: reference to a compiler-generated field
        EventHandler<T> eventHandler = ProgressChanged;
        if (action != null)
            action(e);
        if (eventHandler == null)
            return;
        eventHandler((object)this, e);
    }
}

这意味着 ProgressEx.Report 将等待方法完成,然后返回。也许不是所有情况下的最佳解决方案,但在这种情况下它对我有用。

要调用它,只需使用 SynchronizationContext.Current 作为构造函数的参数创建 ProgressEx。但是,它必须在 UI 线程中创建,因此正确的 SynchronizationContext 会被传入。例如new ProgressEx&lt;MyDataObject&gt;(SynchronizationContext.Current)

【讨论】:

  • 实际上,这breaks IProgress 应该以线程安全的方式向任何想要通知的人报告进度——即不强制线程和侦听器之间进行任何同步。另一方面,此类将阻塞,甚至可能导致死锁等待订阅者。为什么不直接调用 `progressCallback.Report` 而不尝试将其包装在任何任务中?
  • 我已经把它包装成一个任务,等待它完成。但是现在我发现我没有做对,所以我会尝试在没有 Task.Run 的情况下调用它。
猜你喜欢
  • 1970-01-01
  • 2021-12-11
  • 1970-01-01
  • 2020-08-03
  • 2021-01-26
  • 2019-04-14
  • 2018-09-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多