Использование TaskCompletionSource в ожидающем вызове Task.Run

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

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

2 ответа

Я не совсем понимаю цель, которую вы пытаетесь достичь. Но проблема в том, что 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;
        }
    }
}

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

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

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

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

Слово асинхронный здесь является ключевым. В вашем случае вы хотите, чтобы отчеты создавались синхронно (Send), а не асинхронно (Post), а Progress<T> class не предлагает конфигурации того, вызывает ли он 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 учебный класс.

Другие вопросы по тегам