Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice on using async / await

Tags:

c#

async-await

Say I have the following class definitions:

public class Calculator
{
    public CalculatorResult Calculate()
    {
        return LongRunningCalculation();
    }

    private CalculatorResult LongRunningCalculation()
    {
        return new CalculatorResult(0.00);
    }
}

public class ClassThatUsesACalculator
{
    private readonly Calculator calculator;

    public ClassThatUsesACalculator()
    {
        this.calculator = new Calculator();
    }

    public void DoWork()
    {
        for (int i = 0; i < 10; i++)
        {
            var result = calculator.Calculate();

            DoSomethingWithCalculationResult(result);

            DoLightWork();

            OnProgressChanged();
        }
    }
}

public partial class Form : Form
{
    public Form()
    {
        InitializeComponent();
    }

    private void Method(object sender, EventArgs e)
    {
        DoWork();
    }

    private void DoWork()
    {
        var calculator = new ClassThatUsesACalculator();
        calculator.ProgressChanged += (s, e) =>
        {
            // Update progressbar
        };

        calculator.DoWork();
    }
}

If I would want to do the work done in DoWork(), on the form, asynchronously I could add a method (GetCalculationTask) that returns a task using Task.Run() and add a async eventhandler i.e. For a button (MethodOne).

Please correct me if I'm wrong, but it seems to me that this would be the only option when the ClassThatUsesACalculator and Calculator classes reside in a library I don't own.

private Task GetCalculationTask(IProgress<CalculatorProgress> progress)
{
    var calculator = new ClassThatUsesACalculator();
    calculator.ProgressChanged += (s, e) =>
    {
        progress.Report(new CalculatorProgress(0));
    };

    return Task.Run(() =>
    {
        calculator.DoWork();
    });
}

private async void MethodOne(object sender, EventArgs e)
{
    IProgress<CalculatorProgress> progress = new Progress<CalculatorProgress>   (UpdateProgressBar);

    await GetCalculationTask(progress);
}

In the case I do own the library I think there are two more options, one of which very much like the first one. Probably due to the lack of my own understanding.

Create a method on on ClassThatUsesACalculator that encapsulates the DoWork() method and then call that from an asynchronous method on the form.

or,

  1. Encapsulate the LongRunningCalculation() on the Calculator class with a Task.Run().

    public Task<CalculatorResult> CalculateAsync()
    {
        return Task.Run(() =>
        {
            return LongRunningCalculation();
        });
    }
    
  2. Create an async method on ClassThatUsesACalculator the calls that awaits the newly created method.

    public async Task DoWorkAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            var result = await calculator.CalculateAsync();
    
            DoSomethingWithCalculationResult(result);
    
            DoLightWork();
    
            OnProgressChanged(); 
        }
    }
    
  3. Create an asynchronous method on the form (MethodThree)

    private async void MethodThree(object sender, EventArgs e)
    {
        IProgress<CalculatorProgress> progress = new Progress<CalculatorProgress>(UpdateProgressBar);
    
        var calculator = new ClassThatUsesACalculator();
        calculator.ProgressChanged += (s, args) =>
        {
            progress.Report(new CalculatorProgress(0));
        };
    
        await calculator.DoWorkAsync();
    }
    

Now, in my opinion the last option would be the best as I would remain more control. But maybe I'm way off and would like someone's opinion or pointers on this as I can only find explanations on how to consume async, but never really how to build methods for others to consume.

like image 901
Wouter Avatar asked Aug 28 '14 15:08

Wouter


1 Answers

As a general rule, push any Task.Run usage as far up the call stack as possible.

What you want to avoid is having a method with an asynchronous signature that is implemented using Task.Run in a reusable component. That's a lying API. I have a blog post on the subject that goes into greater detail.

If you control the classes in question, I recommend using IProgress<T> instead of events for progress updates. IProgress<T> works just fine with synchronous code as well as asynchronous:

public void DoWork(IProgress<CalculatorProgress> progress = null)
{
  for (int i = 0; i < 10; i++)
  {
    var result = calculator.Calculate();

    DoSomethingWithCalculationResult(result);

    DoLightWork();

    if (progress != null)
      progress.Report(new CalculatorProgress(...));
  }
}

Then using it is quite straightforward:

private async void MethodTwo(object sender, EventArgs e)
{
  IProgress<CalculatorProgress> progress = new Progress<CalculatorProgress>(UpdateProgressBar);

  var calculator = new ClassThatUsesACalculator();

  await Task.Run(() => calculator.DoWork(progress));
}

That keeps the Task.Run usage in the component that needs it - the UI layer - and out of the business logic.

like image 108
Stephen Cleary Avatar answered Nov 02 '22 11:11

Stephen Cleary