Использование TaskCompletionSource в ожидании вызова Task.Run - PullRequest
2 голосов
/ 16 января 2020

У меня неожиданное поведение, на которое я хотел бы пролить свет. Я создал простой пример, чтобы продемонстрировать проблему. Я вызываю функцию asyn c, используя Task.Run, которая будет непрерывно генерировать результаты, и использует IProgress для доставки обновлений в пользовательский интерфейс. Но я хочу подождать до тех пор, пока пользовательский интерфейс фактически не обновится, чтобы продолжить, поэтому я попытался использовать TaskCompletionSource, как предлагалось в некоторых других публикациях (это казалось несколько похожим: Можно ли ожидать событие вместо другого асинхронного c метода? .) Я ожидаю, что начальный 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

Я предполагаю, что я неправильно понимаю что-то о Задачах и жду и как они работают. Я хочу, чтобы StartDataPush был отдельным потоком, и я понимаю, что это так. Мое конечное использование является несколько более сложным, поскольку оно включает в себя тяжелые вычисления, обновление пользовательского интерфейса WPF и события, сигнализирующие о его завершении, но механика остается той же. Как мне достичь того, что я пытаюсь сделать?

Ответы [ 2 ]

3 голосов
/ 17 января 2020

Я не совсем понимаю цель, которую вы пытаетесь достичь. Но проблема в StartDataPu sh, возвращающем пустоту. Единственный раз, когда asyn c должен возвращать 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;
        }
    }
}
1 голос
/ 17 января 2020

Согласно документации класса Progress<T>:

Любой обработчик, предоставленный конструктору, вызывается через экземпляр SynchronizationContext, захваченный при создании экземпляра. Если на момент создания нет текущего SynchronizationContext, обратные вызовы будут вызываться на ThreadPool.

Фраза "вызывается через SynchronizationContext" немного расплывчато. На самом деле происходит то, что метод SynchronizationContext.Post вызывается.

При переопределении в производном классе отправляет асинхронное сообщение в контекст синхронизации ,

Слово асинхронный является ключевым здесь. В вашем случае вы хотите, чтобы отчеты создавались синхронно (Send), а не асинхронно (Post), а класс Progress<T> не предлагает настройки для того, вызывает ли он Send или Post метод захваченного SynchronizationContext.

К счастью, синхронная реализация 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.

...