Async/await для долго работающих методов API с прогрессом/отменой

Изменить Я полагаю, что правильный способ заставить await вызывать воркер асинхронно — это Task.Run, например:


await Task.Run(() => builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress)));

Немного информации с сайта http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx.


это должно быть легко, но я новичок в async/await, так что терпите меня. Я создаю библиотеку классов, предоставляющую API с некоторыми длительными операциями. В прошлом я использовал BackgroundWorker для обработки отчетов о ходе выполнения и отмены, как в этом упрощенном фрагменте кода:


public void DoSomething(object sender, DoWorkEventArgs e)
{
            BackgroundWorker bw = (BackgroundWorker)sender;
            // e.Argument is any object as passed by consumer via RunWorkerAsync...

            do
            {
                // ... do something ...

                // abort if requested
                if (bw.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                } //eif

                // notify progress
                bw.ReportProgress(nPercent);
            }
}

и код клиента был таким:


BackgroundWorker worker = new BackgroundWorker
{ WorkerReportsProgress = true,
    WorkerSupportsCancellation = true
};
worker.DoWork += new DoWorkEventHandler(_myWorkerClass.DoSomething);
worker.ProgressChanged += WorkerProgressChanged;
worker.RunWorkerCompleted += WorkerCompleted;
worker.RunWorkerAsync(someparam);

Теперь я хотел бы использовать новый асинхронный шаблон. Итак, прежде всего, вот как я напишу простой долгоиграющий метод в своем API; здесь я просто читаю файл построчно, просто чтобы эмулировать реальный процесс, когда мне нужно преобразовать формат файла с некоторой обработкой:


public async Task DoSomething(string sInputFileName, CancellationToken? cancel, IProgress progress)
{
    using (StreamReader reader = new StreamReader(sInputFileName))
    {
        int nLine = 0;
        int nTotalLines = CountLines(sInputFileName);

        while ((sLine = reader.ReadLine()) != null)
        {
            nLine++;
            // do something here...
      if ((cancel.HasValue) && (cancel.Value.IsCancellationRequested)) break;
      if (progress != null) progress.Report(nLine * 100 / nTotalLines);
        }
        return nLine;
    }
}

Для примера предположим, что это метод класса DummyWorker. Теперь вот мой клиентский код (тестовое приложение WPF):


private void ReportProgress(int n)
{
    Dispatcher.BeginInvoke((Action)(() => { _progress.Value = n; }));
}

private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
    OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
    if (dlg.ShowDialog() == false) return;

        // show the job progress UI...

    CancellationTokenSource cts = new CancellationTokenSource();
    DummyWorker worker = new DummyWorker();
    await builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress));

    // hide the progress UI...
}

Реализация интерфейса IProgress взята с http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html, чтобы вы могли ссылаться на этот URL. Во всяком случае, в этом тесте использования пользовательский интерфейс эффективно заблокирован, и я не вижу никакого прогресса. Итак, какова будет полная картина для такого сценария со ссылкой на потребляющий код?


person Naftis    schedule 23.10.2012    source источник
comment
Да, Task.Run() это способ сделать это. Или, возможно, Task.Factory.StartNew() с установленной опцией LongRunning, если действие действительно долгое.   -  person svick    schedule 23.10.2012


Ответы (1)


Как отмечено в верхней части этого сообщения в блоге, информация в этом сообщении устарела. Вам следует использовать новый IProgress<T> API, представленный в .NET 4.5.

Если вы используете блокирующий ввод-вывод, сделайте блокировку основного метода:

public void Build(string sInputFileName, CancellationToken cancel, IProgress<int> progress)
{
  using (StreamReader reader = new StreamReader(sInputFileName))
  {
    int nLine = 0;
    int nTotalLines = CountLines(sInputFileName);

    while ((sLine = reader.ReadLine()) != null)
    {
      nLine++;
      // do something here...
      cancel.ThrowIfCancellationRequested();
      if (progress != null) progress.Report(nLine * 100 / nTotalLines);
    }

    return nLine;
  }
}

а затем оберните его в Task.Run, когда вы его вызываете:

private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
  OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
  if (dlg.ShowDialog() == false) return;

  // show the job progress UI...

  CancellationTokenSource cts = new CancellationTokenSource();
  DummyWorker worker = new DummyWorker();
  var progress = new Progress<int>((_, value) => { _progress.Value = value; });
  await Task.Run(() => builder.Build(dlg.FileName, cts.Token, progress);

  // hide the progress UI...
}

В качестве альтернативы вы можете переписать Build для использования асинхронных API, а затем просто вызывать его непосредственно из обработчика событий, не заключая его в Task.Run.

person Stephen Cleary    schedule 24.10.2012
comment
Спасибо, я сделал блокировку своего метода, удалив асинхронность и изменив тип возвращаемого значения на void. - person Naftis; 25.10.2012
comment
Обратите внимание, что вам не разрешен доступ к каким-либо элементам пользовательского интерфейса формы из метода Build(). - person Lefteris E; 07.10.2013
comment
@LefterisE: Вот для чего нужен отчет о прогрессе. - person Stephen Cleary; 07.10.2013
comment
@StephenCleary Не могли бы вы подробнее рассказать об асинхронных API, а затем просто вызывать их непосредственно из обработчика событий? Я всегда думал, что (рекомендуется) сделать это, обернув синхронную задачу в Task.Run(). - person Syaiful Nizam Yahya; 26.01.2014
comment
@publicENEMY: Вовсе нет; Task.Run следует использовать только для синхронного кода, вызываемого из потока пользовательского интерфейса, который вы не можете сделать асинхронным. Лучше начать с листьев с таких вещей, как HTTP-запросы и доступ к БД, заставить их использовать *Async методы и позволить асинхронному коду расти в сторону пользовательского интерфейса. - person Stephen Cleary; 26.01.2014