5

I'm trying to understand some more about async/await and particularly how the compiler knows to "pause" at an async method and await without spawning additional threads.

As an example, let's say I have an async method like

DoSomeStuff();
await sqlConnection.OpenAsync();
DoSomeOtherStuff();

I know that await sqlConnection.OpenAsync(); is where my method gets "suspended" and the thread that invoked it returns to the thread pool and once the Task that is tracking the connection opening completes then an available thread is found to run DoSomeOtherStuff().

| DoSomeStuff() | "start" opening connection | ------------------------------------ | 
| ---------------------------------------------------------- | DoSomeOtherStuff() - |

Here's where I get confused. I look at the source code of OpenAsync (https://referencesource.microsoft.com/#System.Data/System/Data/Common/DBConnection.cs,e9166ee1c5d11996,references) and it's

    public Task OpenAsync() {
        return OpenAsync(CancellationToken.None);
    }

    public virtual Task OpenAsync(CancellationToken cancellationToken) {
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>();

        if (cancellationToken.IsCancellationRequested) {
            taskCompletionSource.SetCanceled();
        }
        else {
            try {
                Open();
                taskCompletionSource.SetResult(null);
            }
            catch (Exception e) {
                taskCompletionSource.SetException(e);
            }
        }

        return taskCompletionSource.Task;
    }

I imagine to see some place where the compiler would know to "cut off" the thread because the task has begun communicating with an external resource, but I don't really see that, and in fact, the Open(); seems to imply that it is synchronously waiting. Can someone explain how this becomes threadless "true async" code?

jsanalytics
  • 13,058
  • 4
  • 22
  • 43
Questionaire
  • 287
  • 1
  • 8
  • 3
    Well, such implementation of OpenAsync is not actually "true async" code. – CodeFuller Nov 12 '17 at 17:20
  • The C# compiler rewrites your code, it turns into *two* methods of a hidden class. The DoSomeOtherStuff() call is in the second method. Any local variables you might have and are used in both parts become fields of that class. Something you can see by using the ildasm.exe utility. – Hans Passant Nov 12 '17 at 17:35
  • @CodeFuller Can you point me to an example of a "true async" implementation of OpenAsync()? Or show how the `OpenAsync()` above could be written to be true async? – Questionaire Nov 12 '17 at 17:51

2 Answers2

6

Your method does not necessarily gets "suspended" on await. If task you are awaiting is already completed (the case in code you provided) - method will just continue as usual. The method you are looking at is really not what is used by SqlConnection, because DbConnection is base class and method OpenAsync is virtual. SqlConnection overrides it and provides real asynchronous implementation. However, not all providers do that and those who do not will indeed use implementation you show in your question.

When such implementation is used - the whole thing will run synchronously without any thread switches. Suppose you have

public async Task Do() {
    DoSomeStuff();
    await sqlConnection.OpenAsync();
    DoSomeOtherStuff();
}

And you use provider which does not provide real async version of OpenAsync. Then when someone calls await Do() - calling thread will perform all the work (DoSomeStuff, OpenAsync, DoSomeOtherStuff). If that is UI thread - it will be blocked for the whole duration (such situation happens often when people use "async" methods for such providers in UI thread assuming that will somehow put the work off the UI thread, which does not happen).

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Can you point me to an example of a "true async" implementation of OpenAsync()? – Questionaire Nov 12 '17 at 17:50
  • @Questionaire here is source code of SqlConnection:http://referencesource.microsoft.com/#System.Data/System/Data/SqlClient/SqlConnection.cs – Evk Nov 12 '17 at 17:54
  • @Questionaire real async is mostly asynchronous IO (files, sockets). You might be interested in reading this: https://blog.stephencleary.com/2013/11/there-is-no-thread.html – Evk Nov 12 '17 at 18:13
5

The benefit of using async-await results from the fact the thread that called the following

await sqlConnection.OpenAsync();

would be released and it would be available to be utilized by from the thread pool that belongs to. If we are talking about for an ASP.NET application the thread would be freed and would be available to serve another incoming HTTP request. This way the threads of the ASP.NET thread pool would be always available to service HTTP requests and wouldn't block for instance for an I/O, like opening a connection to a database and executing some SQL statement.

Update

It should be stated here that if the task you are going to await has been completed, your code would run synchronously.

Christos
  • 53,228
  • 8
  • 76
  • 108
  • Your second paragraph, _Regarding the Open()_, is misleading as `Open()` is not required to run on a background thread. The implementation shown is synchronous and does not, on its own, change context. `OpenAsync` and `Open` run on the same context and will block the single thread associated with the call. – JSteward Nov 12 '17 at 21:10
  • @JSteward I am not sure that your argument is correct. As it is stated here https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/await ***An await expression does not block the thread on which it is executing**. Instead, it causes the compiler to sign up the rest of the async method as a continuation on the awaited task. Control then returns to the caller of the async method. When the task completes, it invokes its continuation, and execution of the async method resumes where it left off.* The whole code in the Task that is awaited would be run in a separate thread. – Christos Nov 12 '17 at 21:30
  • That behavior only applies if the implementation of the `Task/Task ` returning method ever yields and represents a true asynchronous method. If the implementation is synchronous and blocking; `await` will not automatically move the work to another context. A blocking implementation will always block regardless of an awaitable return type. – JSteward Nov 12 '17 at 21:46
  • @JSteward I tottaly removed the corresponding part of `Regarding the Open`, since as it seems it's a bit misleading. By the way an excellent post I found and I would like to share it is this https://stackoverflow.com/questions/37419572/if-async-await-doesnt-create-any-additional-threads-then-how-does-it-make-appl. Thanks – Christos Nov 12 '17 at 22:08