2

我遇到了一些意想不到的行为,我想对此有所了解。我创建了一个简单的例子来演示这个问题。我使用 调用异步函数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 以及发回通知它已完成的事件,但机制是相同的。我怎样才能实现我想要做的事情?

4

2 回答 2

3

我没有完全理解你想要达到的目标。但问题是 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;
        }
    }
}
于 2020-01-16T21:36:47.203 回答
1

根据Progress<T>该类的文档:

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

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

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

异步这个词是这里的关键。在您的情况下,您希望报告同步发生(Send),而不是异步发生(Post),并且Progress<T>该类不提供关于是否调用捕获的Sendor方法的配置。PostSynchronizationContext

幸运的是实现同步IProgress<T>是微不足道的:

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类进行技巧。

于 2020-01-17T11:14:25.480 回答