_ = Task.Run vs async void | Task.Run vs Async Sub

Viewed 2655

In a console application. I need to load some long running code (network-stuff, REST calls) off my main thread. I want to pass it to a background thread and don't block the calling thread. I will invoke events in that method to handle it's result.

Is there any difference beetween doing this,

private async Task DoSomethingAsync() {
    // Doing long running stuff
}
public async Task MainThreadAsync() {
    _ = Task.Run(async () => await DoSomethingAsnyc());
    // Continue with other stuff and don't care about DoSomethingAsync()
}

and doing this?

private async void DoSomethingAsync() {
    // Doing long running stuff
}
public async Task MainThreadAsync() {
    DoSomethingAsync();
    // Continue with other stuff and don't care about DoSomethingAsync()
}

VB.Net:

Private Async Function DoSomethingAsync() As Task
    ' Doing long running stuff
End Function

Public Async Function MainThreadAsync() As Task 
    Task.Run(Async Function() As Task
                 Await DoSomethingAsync()
             End Function)
    ' Continue with other stuff and don't care about DoSomethingAsync()
End Function

vs

Private Async Sub DoSomethingAsync()
    ' Doing long running stuff
End Sub

Public Async Function MainThreadAsync() As Task 
    DoSomethingAsync()
    ' Continue with other stuff and don't care about DoSomethingAsync()
}

Or is there even a better way? Also, is there a difference beetween c# and vb.net in this regard?

3 Answers

Firstly: do not use async void. I realize that it expresses the semantics of what you want, but there are some framework internals that actively explode if they encounter it (it is a long, uninteresting story), so: don't get into that practice.

Let's pretend that we have:

private async Task DoSomething() {...}

in both cases, for that reason.


The main difference here is that from the caller's perspective there is no guarantee that DoSomething won't run synchronously. So in the case:

public async task MainThread() {
    _ = DoSomething(); // note use of discard here, because we're not awaiting it
}

DoSomething will run on the main thread at least as far as the first await - specifically, the first incomplete await. The good news is: you can just add:

await Task.Yield();

as the first line in DoSomething() and it is guaranteed to return immediately to the caller (because Task.Yield is always incomplete, essentially), avoiding having to go via Task.Run. Internally, Task.Yield() does something very similar to Task.Run(), but it can skip a few unnecessary pieces.

Putting that all together - if it was me, I would have:

public async Task MainThread() {
    _ = DoSomething();

    // Continue with other stuff and don't care about DoSomething()
}
private async Task DoSomething() {
    await Task.Yield();

    // Doing long running stuff
}

Regarding c#, this case:

private async void DoSomething() {
    // Doing long running stuff
}
public async task MainThread() {
    DoSomething();
    // Continue with other stuff and don't care about DoSomething()
}

will run synchronously, because what can be awaited is Task and there isn't any Task created.

However this code:

private async task DoSomething() {
    // Doing long running stuff
}
public async task MainThread() {
    _ = Task.Run(async () => await DoSomething());
    // Continue with other stuff and don't care about DoSomething()
}

will run asynchronously (fire-firget) as you are explicitly creating and starting the new Task

Yes, there is a difference. But first lets follow the guidelines and append the Async suffix to the asynchronous methods DoSomething and MainThread:

private async Task DoSomethingAsync() {
    // Doing long running stuff
}
public async Task MainThreadAsync() {
    _ = Task.Run(async () => await DoSomethingAsync());
    // Continue with other stuff and don't care about DoSomethingAsync()
}

This one ensures that the DoSomethingAsync will run in a ThreadPool thread from start to finish. The main thread will not run even a single line of the DoSomethingAsync method. It will just schedule a Task to run in the ThreadPool, which is a minuscule job measured in nanoseconds, and then continue executing the other stuff. A possible exception in the DoSomethingAsync method will never be observed. Unless you handle the TaskScheduler.UnobservedTaskException event (to get a non-deterministically delayed notification), or change a specific setting in App.config, or you are running in .NET Framework 4.0.

private async void DoSomething() {
    // Doing long running stuff
}
public async Task MainThreadAsync() {
    DoSomething();
    // Continue with other stuff and don't care about DoSomething()
}

This one will start running the DoSomething in the main thread, and will return when it encounters an await for a non-completed awaitable. Conceptually you could say that the DoSomething is composed by two parts, the synchronous part and the asynchronous part. The first part will run in the main thread, and the second part will run in a ThreadPool thread (because your app is a Console app and has no SynchronizationContext unless you install one manually).

The DoSomething is async void, so a possible exception in the DoSomething method will be thrown in the current SynchronizationContext, or in the ThreadPool if there is none installed. Which means that your Console app will crash uncontrollably, after raising the AppDomain.UnhandledException event. Which in some cases may be exactly what you want. For example if you are 100% sure that the DoSomething should never throw in normal conditions, then crashing immediately may be preferable to letting the app continue running with a potentially corrupted internal state. In general though you should try to minimize the usage of both fire-and-forget tasks and async-void methods, because they make your program more confusing and unpredictable.

Related