9

You have built a complex calculation algorithm. It takes quite some time to complete and you want to make sure that your application remains responsive. What do you do?

  • A. Use async/await.
  • B. Run the code synchronously.
  • C. Use Task.Run.
  • D. Use a BackgroundWorker.

The answer is C. However, can someone please explain why A) is incorrect? Because the question does not say that the complex algorithm is CPU bound. If it was CPU bound, then we will have to use Tasks (a reasoning I don't understand very well too, though I know that tasks do help cause the current thread to get suspended until they complete). Additionally, please explain how to decide when to use async/await, and use Tasks.


  • False dichotomy; the answer is both A and C. Note that on ASP.NET, the answer is B. - Stephen Cleary

3 답변


3

What do you do?

A. Use async/await.

B. Run the code synchronously.

C. Use Task.Run.

D. Use a BackgroundWorker.

The async/await feature in C# 5.0 was implemented in order to make asynchronous code "as easy" as writing synchronous code. If you look into WinAPI, you'll see that almost all async endpoints expose a fully async api, meaning there is no thread. Looking further you'll notice that those same endpoints are doing I/O bound operations.

Assuming your algorithm is CPU bound, and is written in a manner which allows effective computation across multiple processors (for example, your algorithm doesn't have any shared state which needs synchronization), you can take advantage of the Task Parallel Library introduced in .NET 4.0. The TPL provides an abstraction over the ThreadPool, which in turn attempts to offload work evenly in a multiprocessor environment.

The await keyword can be used on any awaitable object, meaning that object implements the GetAwaiter method. You would want to use await when you are using an awaitable and want execution to resume when that awaitable is done doing its work. A Task implements the awaitable pattern and is used to describe a unit of work which will be completed in the future. You can use Task.Run and await, when you want to offload a unit of work on a worker thread and you want to return control to the calling method until that work is done.

Another way of dispatching CPU bound work across multiple processors is to use the Parallel class which exposes Parallel.For and Parallel.ForEach.

On a side note, a BackgroundWorker can also be used to offload work on a background thread, if thats what the person asking the question was after. It is recommended to use the TPL since .NET 4.0 was released.

To conclude, using the Task Parallel Library is the recommended way to offload work to background threads. You may use them in combination with the Parallel library to maximize the parallelism of your algorithm. After doing so, test your code to make sure the overhead of using multiple threads doesn't outweigh the time it takes to run your algo synchronously.

Edit

As Stephan mentioned in the comment, you may combine both Parallel.ForEach and Task.Run to offload a parallel loop inside a background thread:

var task = Task.Run(() => Parallel.ForEach(.. //Loop here));


  • It can also be useful to combine approaches: await Task.Run(() => Parallel.ForEach(...)) will do parallel work on background threads that the UI treats asynchronously. - Stephen Cleary

2

I suppose "calculation" implies CPU bound algorithm in the absence of other information. If the algorithm is IO bound async/await is not only acceptable but the correct answer.


  • That's true. Of course you can go for a Dispatcher approach. - Stilgar
  • @DimitarDimitrov, if you're using Task.Run, the delegate passed to it will likely execute on a thread other than the UI thread, so your UI will remain responsive in any case where you don't block on the task's completion via Wait() or Result. How you choose to handle task's completion and communicate with the thread that called Task.Run in the first place, however, is up to you: you can use await, or you can use task continuations with the appropriate TaskScheduler, or you can explicitly capture the original SynchronizationContext and post back to it from within your delegate, - Kirill Shlenskiy
  • (cont'd) or you can use Dispatcher.Invoke or Control.Invoke, or you can roll your own AsyncEnumerator (blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx). My point is, there's about a million ways to do it, and async/await just happens to be one of them, even if it is the first thing that comes to everyone's mind (which is quite understandable). - Kirill Shlenskiy
  • @KirillShlenskiy Sure, you're 100% right, I agree (even though you'r being a bit pedantic), but the fact that the question is garbage still remains (IMHO). - Dimitar Dimitrov
  • @DimitarDimitrov, agreed. - Kirill Shlenskiy

1

I believe the question assumes you have an algorithm that has been programmed synchronously. The question mentions that it is a 'calculation' which vaguely implies that it is CPU-bound. It also very vaguely implies that the algorithm is written synchronously. E.g:

public int CalculateStuff() {
     ....
}

What I would consider is creating an asynchronous counterpart to this method:

public async Task<int> CalculateStuffAsync() {
      return await Task.Run(() => CalculateStuff());
}

Then in your user interface:

LoadingIndicator.IsEnabled = true;
ResultTextBox.Text = await CalculateStuffAsync();
LoadingIndicator.IsEnabled = false;

Hence the correct answer would probably be both A and C. In any case the question is too vague to draw any solid conclusion.


  • @DimitarDimitrov agree, all except B are technically valid - Bas
  • @BasBrekelmans, providing public xxxAsync wrappers for CPU-bound work is poor design. It's not like the caller can't wrap CalculateStuff in Task.Run if it needs to. Also consider the following: CalculateStuffAsync().Wait(). Without ConfigureAwait(false) you've just introduced a deadlock bug that wasn't there until you wrote your async method, for pretty much zero benefit. - Kirill Shlenskiy
  • @DimitarDimitrov, don't take my word for it. Run your bit of code in an application which installs a non-null SynchronizationContext (i.e. Windows Forms/WPF) and see what happens. This is due to the fact that your await needs to transition to the original SynchronizationContext before the result of the async operation can be returned. If you block the thread via Task.Wait, that transition will never occur because the thread will be busy waiting for the task (which will never complete). Checkmate. Take out async/await and just return the task directly, and it's solved. - Kirill Shlenskiy
  • (or use ConfigureAwait(false) obviously) - Kirill Shlenskiy
  • @DimitarDimitrov, no problem; and please accept my apologies for incorrectly addressing my reply. I confused you for Bas Brekelmans when I wrote it. - Kirill Shlenskiy

Related

Latest