I’m working on something that’s challenging my understanding of async await workflow.
The main point of the confusion is this, there seems to be a subtle difference between a method completion/completing vs control returning to the caller.
So start from the basics, consider, the below excerpt, from [There is No Thread][1]:
private async void Button_Click(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}
The UI is clearly responsive because “control is returned back to the caller”, thus, I can move the UI and play with other buttons.
However, has the method actually completed?
If I modify the code slightly:
private async void Button_Click(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
..DoMoreWork()..
}
Despite not blocking the UI thread, await, is clearly a blocking call in the context of the continuation instructions not executing and method has not run to completion.
Which leads me to the source of my confusion: [GRPC services and methods][2]
A server streaming method has the request message as a parameter. Because multiple messages can be streamed back to the caller, responseStream.WriteAsync is used to send response messages. A server streaming call is complete when the method returns.
Slight modification of MSFT’s sample code:
public override async Task StreamingFromServer(ExampleRequest request,
IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
for (var i = 0; i < 5; i++)
{
await responseStream.WriteAsync(new ExampleResponse());
await Task.Delay(TimeSpan.FromSeconds(100));
}
}
In the context of a UI thread, this piece of code, would have returned control back to the GRPC thread invoking the first await instruction. Yet, the GRPC client is patiently waiting for a response, thus, the streaming call is still active or incomplete. Presumably, when the network device driver has finished writing to the socket an interrupt is progressively bubbled back to user space or something similar etc.
If I remove, the await keyword, and the method returns immediately the stream is immediately closed and I actually get a log information saying something of the sorts: “GRPC stream ended”
Which leads me to the question I asked above, how does the runtime distinguish between returning control back to the caller vs method finished to completion?
If I try to think about this logically.. I can assume GRPC works akin to UI threading model.
So in GRPC/UI mode:
- A Task is returned
-.NET runtime stores Task in some state machine queue - UI/GRPC thread continues process message queue async calls finishes and borrows a IOCP
thread to continue processing the relevant state machine context (how this works Im
still not completely clear) - The Task state has info about whether the promise to complete has been completed or
still pending.
If I’m off the mark is there documentation I can read on this (subtle) difference?
-TIA
[1]: https://blog.stephencleary.com/2013/11/there-is-no-thread.html
[2]: https://learn.microsoft.com/en-us/aspnet/core/grpc/services?view=aspnetcore-8.0