Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating background worker to async-await

So this is how I currently use background worker to save a lot of stuff to file while presenting the user with a progress bar and preventing any changes to the UI while saving is in progress. I think I've captured the essential features. The modal ProgressWindow displays a progress bar and not much else. How would I go about changing this to async-await pattern, if I had to?

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();
}
like image 586
Graham Avatar asked Feb 10 '16 12:02

Graham


People also ask

Is BackgroundWorker obsolete?

BackgroundWorker is explicitly labeled as obsolete in .

Does async await improve performance?

The main benefits of asynchronous programming using async / await include the following: Increase the performance and responsiveness of your application, particularly when you have long-running operations that do not require to block the execution.

What is the difference between task run and async await?

An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method. The async and await keywords don't cause additional threads to be created.

Is it mandatory to use await with async?

await waits for async to resolve or reject. if you dont need the results, you wont need to await. If you don't await it (or otherwise handle the error, e.g. with . catch ), and it errors, you have an "unhandled promise rejection".


2 Answers

I have a blog series that covers this in detail.

In short, BackgroundWorker is replaced by Task.Run, and ReportProgress (and friends) is replaced by IProgress<T>.

So, a straightforward translation would look like this:

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
}

Notes for improvements:

  • It's not usually a good idea to have background threads reaching into the UI (// get some stuff from UI). It would probably work better if you could collect all the information from the UI before calling Task.Run and just pass it into the Save method.
like image 149
Stephen Cleary Avatar answered Oct 04 '22 11:10

Stephen Cleary


I guess the reason why you let another thread do the time consuming stuff is because you want to keep the user interface responsive. Your method will meet this requirement.

The advantage of using async-await is that the code will look more synchronous, while the user interface seems to be responsive. You don't have to work with events and functions like Control.IsInvokeRequired, because it is the main thread that will do the work.

The disadvantage of async-await is that as long as the main thread is really doing something (= not awaiting for a task to finish), your UI is not responsive.

Having said that, making a function async is easy:

  • declare the function async
  • Instead of void return Task and instead of TResult return Task < TResult >.
  • The only exception to this rule are event handlers. An async event handler returns void.
  • Do your stuff sequentially and whenever possible call async versions of other functions.
  • Calling this async function does not execute it immediately. Instead it is scheduled to be executed as soon as a thread in the pool of available thread is ready to do so.
  • This means that after your thread scheduled the task it is free to do other things
    • When your thread needs the result of the other task await for the tak.
    • The return of await Task is void, the return of await Task < TResult > is TResult.

So to make your function async:

The async SaveFile function is easy:

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(...)
    }

The SaveButtonClick async is also easy. Because of all my comment it seems a lot of code, but in fact it is a small function.

Note that the function is an event handler: return void instead of 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 );

The task is scheduled to run as soon as a thread in the thread pool is free. Meanwhile the main thread has time to do other things, like showing and updating the progress window.

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

Now repeatedly wait a sec and adjust progress. Stop as soon as the saveFileTask task is finished.

We can't just let the main thread wait for the task to finish because that would stop UI from being responsive, besides the main thread should repeatedly update the progressbar.

Solution: Don't use the Task.Wait functions, but the Task.When functions. The difference is that Task.When functions return awaitable Tasks, and thus you can await for the task to finish, thus keeping the UI responsive.

The Task.When funcitons don't have a timeout version. For this we start a Task.Delay

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

The Task.WhenAny stops as soon as the fileSaveTask is completed, or if the delay task is completed.

Things to do: react on errors if fileSave encounters problems. Consider returning a Task < TResult > instead of Task.

TResult fileSaveResult = fileSaveTask.Result;

or throw an exception. The main window thread catches this as an AggregateException. The InnerExceptions (plural) contain the exceptions thrown by any of the tasks.

If you need to be able to stop the saving process, you need to pass a CacellationToken to every function and let the SaveFile

like image 22
Harald Coppoolse Avatar answered Oct 04 '22 09:10

Harald Coppoolse