Использование 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
учебный класс.