11

I am trying to grok how async and await works in C#.

Consider the two snippets below:

var appIdTask = GetAppIdAsync();
var clientSecretTask = GetClientSecretAsync();
var appId = await appIdTask;
var clientSecret = await clientSecretTask;
Execute(appId, clientSecret);

and

var appId = await GetAppIdAsync();
var clientSecret = await GetClientSecretAsync();
Execute(appId, clientSecret);

These two snippets have different implications. Correct?

First one will make the Get calls in parallel whereas the second one will make the calls in serial?

From my understanding, the await keyword on the first call blocks the execution on the second call.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
ashwnacharya
  • 14,601
  • 23
  • 89
  • 112
  • 3
    When learning, it may help to use clearer terminology. `await` doesn't "block" execution, but it does "pause" its method. And I prefer using the term "concurrent" (more than one thing at a time) for the first block rather than "parallel" (a more specific form of concurrency using multithreading). – Stephen Cleary May 08 '20 at 11:52

3 Answers3

10

It doesn't "block" in the traditional sense of "halting the current thread in the current state until some signal is received" (one of the main objectives of async is to increase throughout by allowing more effective use of pool threads, by having them not all sat idly waiting for IO), but yes: if the method reports that it is incomplete, the execution will be suspended by await, and resumed (quite likely on a different shared thread) when the async result is available.

So yes, semantically it has the effect of not running the two things concurrently (note this only applies if the first call is truly asynchronous).

Note that many APIs do not expect multiple concurrent async operations, and will have undefined behaviour in the first example.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Maybe "block" was the wrong word to use. Will the first one run faster than the second one? – ashwnacharya May 08 '20 at 08:07
  • 1
    @ashwnacharya that's entirely up to the methods being called, and what they do; as I say, it would be perfectly valid for them to outright fail here - many APIs **do not expect** multiple concurrent calls; and if it *does* work, it depends on how it is implemented; it might work genuinely concurrently, which *might* improve perf *in some cases*, or it might end up essentially "pipelined" - in which case you *might* save some latency costs *in some cases*. It is entirely situational, and I can think of scenarios for every possible outcome. – Marc Gravell May 08 '20 at 10:36
  • Two of them are making HTTP get requests that are independent of each other @Marc – ashwnacharya May 08 '20 at 13:43
  • @ashwnacharya No one here can answer about speed since that involves other things beyond this code. The first code can run GetAppIdAsync and GetClientSecretAsync in parallel if those functions allow that, the second code will not run GetClientSecretAsync until GetAppIdAsync completes. – Moby Disk May 08 '20 at 16:20
  • @ashwnacharya regarding the first code snippet, if the two requests are independent of each other (targeting different servers), and the data sent and received from both servers are minimal (to exclude the possibility that the bandwidth is the bottleneck), then the expected duration is equal to the maximum duration of both requests. For example if the durations are 5 sec and 10 sec, then the snippet will complete in 10 sec. – Theodor Zoulias May 08 '20 at 18:51
  • "many APIs do not expect multiple concurrent async operations": could you please report one or two examples of APIs that do not support concurrent asynchronous operations? Is this fact documented? – Luca Cremonesi May 11 '20 at 15:39
  • 1
    @LucaCremonesi sure! `Socket`, `NetworkStream`, `FileStream`, `SqlConnection`, ... ultimately, an unawaited async operation is semantically similar to running two regular (non-async) operations *from different threads* on the same object at the same time. – Marc Gravell May 11 '20 at 15:50
  • @MarcGravell thank you for the response. Regarding the `FileStream` and `SqlConnection` classes I could not find anything in the documentation that states that are not thread safe and that require synchronization of multiple threads. – Luca Cremonesi May 11 '20 at 16:21
  • @LucaCremonesi there used to be a bolierplate "unless explicitly stated otherwise, all instance methods of this type are not thread safe" style warning on *all* types; that should basically be your default assumption at all times, on any type; and "unawaited" is the same a different threads for this purpose; basically, if it doesn't explicitly say that you can: you shouldn't assume it – Marc Gravell May 11 '20 at 16:25
  • @MarcGravell - I remember that the old class documentation always reported a section about thread safety and I was surprised to not find it in the new documentation. This could be a source of confusion and I am wondering why they decided to remove it. And technically, even using two consecutive awaits is accessing the object with multiple threads, in general, as the second await could be scheduled on a second thread. We should assume that all the classes are safe on a rental threading model. – Luca Cremonesi May 11 '20 at 16:35
  • @LucaCremonesi but it *isn't* a rental threading model; and if you don't "await" a task-based API, that's like returning your car to the rental place, but *still using it anyway*, and wondering why it doesn't work well with two people trying to drive – Marc Gravell May 12 '20 at 00:16
  • @MarcGravell I know that calling asynchronous methods on the same object without awaiting is not thread safe in general. However, even awaiting each call is not thread safe since you are accessing the same object with different threads. It would be thread safe *only* if you assume that the class is thread safe on a rental threading model. Although there is nothing in the documentation that states this, the async/await feature will be unusable if we cannot assume that classes that expose asynchronous methods are thread safe on a rental threading model. – Luca Cremonesi May 12 '20 at 17:34
  • @Luca when you await, you aren't in control of the threading model - the underlying async code is. And ultimately, you need to stop thinking in terms.of threads and start thinking in terms of logical execution flows (or perhaps: tasks). Instead of threads, it becomes tasks that can't be overlapped. – Marc Gravell May 12 '20 at 20:54
3

One important rule about async/awaits is that a method with the async modifier will run synchronously until it gets to the first incomplete Task.

Async is not about parallelism but asynchronism. It's not based on the fact that the method is async but on the state of the Task being returned.

I invite you to take a look at this post, and its code example.

hardkoded
  • 18,915
  • 3
  • 52
  • 64
1

So in the first snippet:


var appIdTask = GetAppIdAsync(); // here we are starting the execution of GetAppId on another thread, no "blocking" the main one
var clientSecretTask = GetClientSecretAsync(); // instantly after starting previous method, run GetClientSecrent on yet another thread
// at this point there are 2 parallel executions on different threads happening
var appId = await appIdTask; // wait until GetAppId has finished and assign the result to appId variable
var clientSecret = await clientSecretTask; // wait until GetClientSecret has finished and assign the result to clientSecret variable
Execute(appId, clientSecret);

And the second snippet:

var appId = await GetAppIdAsync(); // start executing GetAppId on different thread (not blocking the main one), and return once it has been completed and assign the result to appId variable
// at this point GetAppIdAsync is completed
var clientSecret = await GetClientSecretAsync(); // start executing GetClientSecret on different thread (not blocking the main one), and return once it has been completed and assign the result to appId clientSecret 
// at this point GetClientSecretAsync is completed
Execute(appId, clientSecret);

If you need to run multiple calls in parallel, I recomend using Task.WhenAll

Jakub Kozera
  • 3,141
  • 1
  • 13
  • 23
  • can you provide an example when it wouldn't? – Jakub Kozera May 08 '20 at 10:05
  • `public async Task GetValue() => 42;`. – Enigmativity May 08 '20 at 10:19
  • 1
    https://blog.stephencleary.com/2013/11/there-is-no-thread.html – Enigmativity May 08 '20 at 10:20
  • yep, thats right, but in context of this question: `Get calls`, it's kinda implicit that the calls are beeing send to some API, with that assumption my answer is correct, isn't it? ;) – Jakub Kozera May 08 '20 at 10:36
  • 4
    "on different threads happening" is *very very* misleading; unfortunately, the choice of words is often crucial when describing tasks, threads, asynchronicity, etc – Marc Gravell May 08 '20 at 10:38
  • My point is that you don't know if `GetAppIdAsync()` is creating a thread or not. By making the statement "here we are starting the execution of GetAppId on another thread" you are promulgating the idea that a thread is being created when you can't possibly know. I'm not saying it's not likely, but it is misleading. – Enigmativity May 08 '20 at 11:03
  • 1
    Had you started with a disclaimer like "assuming that each of `GetAppIdAsync` and `GetClientSecretAsync` start a task running on other threads" then you could get on with your answer just fine. – Enigmativity May 08 '20 at 11:04