簡體   English   中英

將后台工作程序更新為async-await

[英]Updating background worker to async-await

因此,這就是我目前使用后台工作程序將大量內容保存到文件中的方式,同時向用戶顯示進度條並防止在保存過程中對UI進行任何更改。 我想我已經捕獲了基本功能。 模態ProgressWindow顯示進度條而不是其他。 如果必須的話,我將如何將其更改為async-await模式?

private ProgressForm ProgressWindow { get; set; }

/// <summary>On clicking save button, save stuff to file</summary>
void SaveButtonClick(object sender, EventArgs e)
{
  if (SaveFileDialog.ShowDialog() == DialogResult.OK)
  {
    if (!BackgroundWorker.IsBusy)
    {
      BackgroundWorker.RunWorkerAsync(SaveFileDialog.FileName);
      ProgressWindow= new ProgressForm();
      ProgressWindow.SetPercentageDone(0);
      ProgressWindow.ShowDialog(this);
    }
  }
}

/// <summary>Background worker task to save stuff to file</summary>
void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
  string path= e.Argument as string;

  // open file

  for (int i=0; i < 100; i++)
  {
    // get some stuff from UI
    // save stuff to file
    BackgroundWorker.ReportProgress(i);
  }

  // close file
}

/// <summary>On background worker progress, report progress</summary>
void BackgroundWorkerProgressChanged(object sender, ProgressChangedEventArgs e)
{
  ProgressWindow.SetPercentageDone(e.ProgressPercentage);
}

/// <summary>On background worker finished, close progress form</summary>
void BackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
  ProgressWindow.Close();
}

我有一個博客系列 ,詳細介紹了這一點。

簡而言之, BackgroundWorkerTask.Run取代, ReportProgress (和朋友)被IProgress<T>取代。

所以,簡單的翻譯看起來像這樣:

async void SaveButtonClick(object sender, EventArgs e)
{
  if (SaveFileDialog.ShowDialog() == DialogResult.OK)
  {
    ProgressWindow = new ProgressForm();
    ProgressWindow.SetPercentageDone(0);
    var progress = new Progress<int>(ProgressWindow.SetPercentageDone);
    var task = SaveAndClose(SaveFileDialog.FileName, progress));
    ProgressWindow.ShowDialog(this);
    await task;
  }
}

async Task SaveAndClose(string path, IProgress<int> progress)
{
  await Task.Run(() => Save(path, progress));
  ProgressWindow.Close();
}

void Save(string path, IProgress<int> progress)
{
  // open file

  for (int i=0; i < 100; i++)
  {
    // get some stuff from UI
    // save stuff to file
    if (progress != null)
      progress.Report(i);
  }

  // close file
}

改進注意事項:

  • 讓后台線程進入UI( // get some stuff from UI )並不是一個好主意。 如果您可以調用Task.Run 之前從UI收集所有信息並將其傳遞給Save方法,那么它可能會更好。

我想你讓另一個線程做的耗時的原因是因為你想讓用戶界面保持響應。 您的方法將滿足此要求。

使用async-await的優點是代碼看起來更加同步,而用戶界面似乎是響應式的。 您不必使用Control.IsInvokeRequired等事件和函數,因為它是將完成工作的主線程。

async-await的缺點是,只要主線程確實在做某事(=不等待任務完成),你的UI就沒有響應。

話雖如此,使函數異步很容易:

  • 聲明函數異步
  • 而不是void返回Task而不是TResult返回Task <TResult>。
  • 此規則的唯一例外是事件處理程序。 異步事件處理程序返回void。
  • 按順序執行您的操作,並盡可能調用其他函數的異步版本。
  • 調用此異步函數不會立即執行它。 相反,它被安排在可用線程池中的線程准備就緒時立即執行。
  • 這意味着在您的線程安排任務后,可以自由地執行其他操作
    • 當你的線程需要其他任務的結果等待tak。
    • 等待任務的返回無效,等待任務<TResult>的返回是TResult。

所以要使你的函數異步:

異步SaveFile函數很簡單:

private async Task SaveFileAsync(string fileName)
{   // this async function does not know
    // and does not have to know that a progress bar is used
    // to show its process. All it has to do is save
    ...
    // prepare the data to save, this may be time consuming
    // but this is not the main thread, so UI still responding
    // after a while do the saving and use other async functions
    using (TextWriter writer = ...)
    {
        var writeTask = writer.WriteAsync (...)
        // this thread is free to do other things,
        // for instance prepare the next item to write
        // after a while wait until the writer finished writing:
        await writeTask;

        // of course if you had nothing to do while writing
        // you could write:
        await writer.WriteAsync(...)
    }

SaveButtonClick異步也很容易。 由於我的所有評論似乎很多代碼,但實際上它是一個小功能。

請注意,該函數是一個事件處理程序:return void而不是Task

private async void SaveButtonClick(object sender, EventArgs e)
{   
    if (SaveFileDialog.ShowDialog() == DialogResult.OK)
    {
        // start a task to save the file, but don't wait for it to finish
        // because we need to update the progress bar
        var saveFileTask = Task.Run () => SaveFileAsync ( SaveFileDialog.FileName );

一旦線程池中的線程空閑,該任務就會被安排運行。 同時主線程有時間做其他事情,比如顯示和更新進度窗口。

        this.ProgressWindow.Visible = true;
        this.ProgressWindow.Value = ...

現在反復等一下,調整進度。 saveFileTask任務完成后立即停止。

我們不能讓主線程等待任務完成,因為這會阻止UI響應,除了主線程應該重復更新進度條。

解決方案:不要使用Task.Wait函數,而是使用Task.When函數。 不同之處在於Task.When函數返回等待的任務,因此您可以等待任務完成,從而保持UI響應。

任務。當功能沒有超時版本時。 為此,我們啟動Task.Delay

    while (!fileSaveTask.IsCompleted)
    {
        await Task.WhenAny( new Task[]
        {
            fileSaveTask,
            Task.Delay(TimeSpan.FromSeconds(1)),
        };
        if (!fileSaveTask.IsCompleted
           this.UpdateProgressWindow(...);
    }

一旦fileSaveTask完成,或者延遲任務完成,Task.WhenAny就會停止。

要做的事:如果fileSave遇到問題,就會對錯誤做出反應。 考慮返回Task <TResult>而不是Task。

TResult fileSaveResult = fileSaveTask.Result;

或拋出異常。 主窗口線程將其捕獲為AggregateException。 InnerExceptions(復數)包含任何任務拋出的異常。

如果您需要能夠停止保存過程,則需要將CacellationToken傳遞給每個函數並讓SaveFile

Stephen Cleary的答案基本上涵蓋了案例。 但是阻塞ShowDialog調用會導致一個復雜的因素阻止正常的async/await流。

所以除了他的回答,我建議你使用以下一般助手功能

public static class AsyncUtils
{
    public static Task ShowDialogAsync(this Form form, IWin32Window owner = null)
    {
        var tcs = new TaskCompletionSource<object>();
        EventHandler onShown = null;
        onShown = (sender, e) =>
        {
            form.Shown -= onShown;
            tcs.TrySetResult(null);
        };
        form.Shown += onShown;
        SynchronizationContext.Current.Post(_ => form.ShowDialog(owner), null);
        return tcs.Task;
    }
}

然后刪除ProgressWindow表單成員並使用以下內容

async void SaveButtonClick(object sender, EventArgs e)
{
    if (SaveFileDialog.ShowDialog() == DialogResult.OK)
    {
        using (var progressWindow = new ProgressForm())
        {
            progressWindow.SetPercentageDone(0);
            await progressWindow.ShowDialogAsync(this);
            var path = SaveFileDialog.FileName;
            var progress = new Progress<int>(progressWindow.SetPercentageDone);
            await Task.Run(() => Save(path, progress));
        }
    }
}

static void Save(string path, IProgress<int> progress)
{
    // as in Stephen's answer
}

請注意,我已將實際的worker方法標記為static ,以防止訪問內部的表單(以及任何UI元素),並僅使用傳遞的參數。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM