【问题标题】:Using TaskCompletionSource Within An await Task.Run Call在 await Task.Run 调用中使用 TaskCompletionSource
【发布时间】:2020-01-16 20:59:41
【问题描述】:

我遇到了一些意料之外的行为,我想解释一下。我创建了一个简单的例子来演示这个问题。我使用Task.Run 调用了一个异步函数,它将不断生成结果,并使用IProgress 向UI 传递更新。但我想等到 UI 真正更新后才能继续,所以我尝试使用其他帖子中建议的 TaskCompletionSource(这似乎有点相似:Is it possible to await an event instead of another async method?。)我期待最初的Task.Run 等待,但是什么正在发生的是内部发生的等待似乎将其向前移动,并且“END”在第一次迭代之后发生。 Start() 是入口点:

public TaskCompletionSource<bool> tcs;

public async void Start()
{
    var progressIndicator = new Progress<List<int>>(ReportProgress);

    Debug.Write("BEGIN\r");
    await Task.Run(() => this.StartDataPush(progressIndicator));
    Debug.Write("END\r");
}

private void ReportProgress(List<int> obj)
{
    foreach (int item in obj)
    {
        Debug.Write(item + " ");
    }
    Debug.Write("\r");
    Thread.Sleep(500);

    tcs.TrySetResult(true);
}

private async void StartDataPush(IProgress<List<int>> progressIndicator)
{
    List<int> myList = new List<int>();

    for (int i = 0; i < 3; i++)
    {
        tcs = new TaskCompletionSource<bool>();

        myList.Add(i);
        Debug.Write("Step " + i + "\r");

        progressIndicator.Report(myList);

        await this.tcs.Task;
    }
}

这样我得到:

BEGIN
Step 0
0 
END
Step 1
0 1 
Step 2
0 1 2 

而不是我想要得到的是:

BEGIN
Step 0
0 
Step 1
0 1 
Step 2
0 1 2 
END

我假设我对 Tasks 和 await 以及它们的工作方式有一些误解。我确实希望 StartDataPush 成为一个单独的线程,我的理解是它是。我的最终用途稍微复杂一些,因为它涉及大量计算、更新到 WPF UI 以及返回它已完成的事件,但机制是相同的。我怎样才能实现我想要做的事情?

【问题讨论】:

  • 你不能有意义地等待async void。让它async Task.
  • 有一条评论说明为什么Progress.Report 在调用时不会run as expected。我说错了,删了。我认为 错了。

标签: c# async-await taskcompletionsource iprogress


【解决方案1】:

我没有完全理解您要达到的目标。但问题是 StartDataPush 返回 void。 async 唯一应该返回 void 的情况是它是一个事件处理程序,否则它需要返回 Task。

以下将在输出方面达到您的预期

public partial class MainWindow : Window
{
    public TaskCompletionSource<bool> tcs;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var progressIndicator = new Progress<List<int>>(ReportProgress);

        Debug.Write("BEGIN\r");
        await StartDataPush(progressIndicator);
        Debug.Write("END\r");
    }


    private void ReportProgress(List<int> obj)
    {
        foreach (int item in obj)
        {
            Debug.Write(item + " ");
        }
        Debug.Write("\r");
        Thread.Sleep(500);

        tcs.TrySetResult(true);
    }

    private async Task StartDataPush(IProgress<List<int>> progressIndicator)
    {
        List<int> myList = new List<int>();

        for (int i = 0; i < 3; i++)
        {
            tcs = new TaskCompletionSource<bool>();

            myList.Add(i);
            Debug.Write("Step " + i + "\r");

            progressIndicator.Report(myList);

            await this.tcs.Task;
        }
    }
}

【讨论】:

  • 整个tcs业务也不需要。 progressIndicator.Report(myList)同步运行,tcs.TrySetResult(true);await this.tcs.Task;可以去掉。
  • 为了 OP 的利益,异步方法必须是 Task 的原因是因为 Task 可等待的。 async/await 是简单的关键字,告诉编译器创建一个IAsyncStateMachine
  • 谢谢,这行得通!我试图做的不是在被调用的异步函数中循环完成之前将代码推进到“END”写入。这段代码在我添加到 TaskCompletionSource 之前有效(在循环中休眠)。此示例代码没有任何意义,但在实际应用程序中却很有意义。 @ColinM 谢谢你回答了我的下一个问题。我很惊讶它在改变之前就起作用了。
  • @GSerg 如果我删除 tcs 部分,我得到 BEGIN Step 0 Step 1 Step 2 0 1 2 0 1 2 0 1 2 END 作为输出。我试图在实际添加下一个值之前报告每个值。
  • @Cloudless 鉴于await StartDataPush(progressIndicator);(没有Task.Run),您可以删除tcsThread.Sleep 并使用await Task.Yield(); 代替,紧跟在progressIndicator.Report(myList); 之后。
【解决方案2】:

根据Progress&lt;T&gt;类的文档:

提供给构造函数的任何处理程序都是通过在构造实例时捕获的SynchronizationContext 实例调用的。如果在构造时没有当前的SynchronizationContext,则将在ThreadPool 上调用回调。

短语“通过SynchronizationContext调用”有点含糊。实际发生的是调用了方法SynchronizationContext.Post

在派生类中重写时,将异步消息分派到同步上下文。

异步这个词是这里的关键。在您的情况下,您希望报告同步发生(Send),而不是异步发生(Post),并且Progress&lt;T&gt; 类没有提供关于是否调用捕获的@ 的SendPost 方法的配置987654334@.

幸运的是,实现同步 IProgress&lt;T&gt; 很简单:

class SynchronousProgress<T> : IProgress<T>
{
    private readonly Action<T> _action;
    private readonly SynchronizationContext _synchronizationContext;

    public SynchronousProgress(Action<T> action)
    {
        _action = action;
        _synchronizationContext = SynchronizationContext.Current;
    }

    public void Report(T value)
    {
        if (_synchronizationContext != null)
        {
            _synchronizationContext.Send(_ => _action(value), null);
        }
        else
        {
            _action(value);
        }
    }
}

只需使用SynchronousProgress 类而不是内置的Progress,您将不再需要使用TaskCompletionSource 类做任何花招。

【讨论】:

  • 我应该澄清SynchronousProgress.Report 方法是一种阻塞方法。此方法的调用者将阻塞,直到 UI 更新。这对于 WPF 应用程序来说可能没问题,但不要在 ASP .NET 应用程序中使用 SynchronousProgress 类,因为它会损害应用程序的可伸缩性。
猜你喜欢
  • 1970-01-01
  • 2021-05-08
  • 1970-01-01
  • 2021-12-02
  • 2022-01-07
相关资源
最近更新 更多