C# 8 Async Streams vs REST/RPC
Asked Answered
K

2

5

I'm sure this question is going to prove my ignorance, but I'm having a hard time understanding this. I'm willing to ask a dumb question to get a good answer.

All of the posts I've read about async streams do a good job of showing off the feature, but they don't explain why it's an improvement over the alternative.

Or, perhaps, when should one use async streams over good old client-server communication?

I can see where streaming the contents of a large file might be a good use for async streams, but many of the examples I've seen use async streams to transmit small bits of sensor data (temperature, for example). It seems like an IoT device with a temperature sensor could just HTTP POST the data to a server, and the server could respond. Why would the server implement async streams in that case?

I can already feel your pain as you struggle to make sense of those words, but please have mercy on me. :)

As requested, here are some examples I've come across that confused me. I'll post more as I find them, but I wanted to go ahead and get you started:

Kirby answered 16/10, 2019 at 12:2 Comment(5)
The two aren't related at all. Async streams are a programming concept that has nothing to do with networking. That programming concept though can be used to make gRPC streams easier to writeArvillaarvin
Maybe you could give a reference to some of the posts you've seen, and tell us what you understand them to say, and then we can try and identify any points of confusion. These are really complimentary concepts, not competing concepts. Here's an MSDN tutorial on the feature; notice how the implementation of the Async stream itself uses "good old client-server communication" insomuch as it uses a standard HTTP PostJonathonjonati
I've updated the question to include a few of the more recent posts I've come across. I'll try to find more... But, then, I guess maybe the real question is, "When are async streams appropriate"? The example @Jonathonjonati gave here is helpful, I think... Is the driving factor that async streams are "pull based" and that they allow intermediate results with cancellation? Obviously, I'm still trying to organize the thoughts in my head and formulate the question. When I started this, I really wasn't sure what to ask, so these comments are helping me. Thanks.Kirby
@AlexDresko no, the two things are unrelated. Async streams is await foreach and IAsyncEnumerable. Nothing at all to do with IO, HTTP and networking. They can be used to make calling gRPC streams easierArvillaarvin
I will say, (and maybe this is a language thing more than anything else) calling them “unrelated” is maybe a bit strong. I would say the “++” operator and “for” loops are related, because I see them often in the same context. Likewise here. I don’t say this to criticize - your answer is spot on - but it may help clarify your point to future readers. These concepts are related insomuch as the are often seen in the same context.Jonathonjonati
A
7

I wanted to write a professional response but the crude one is probably needed too:

Forget you ever heard about async streams. What were they thinking?

Call it await foreach, or async enumerables or async iterators. It has nothing to do with IO and streams.

The term is used because it exists in other languages, not because it has anything to do with IO. In Java for example, streams are Java's implementation of C#'s IEnumerable. So, to ease adoption by future Android devs, C# adopted Java's bad idea.

We can look at the language design meetings for the actual justification for this term I guess.


Serious original answer

There's no vs. It's like contrasting automatic gear boxes and cars. Cars can have automatic gear boxes, they aren't used instead of gear boxes.

Async streams is purely a programming concept that allows the creation of async iteratos. It's the feature that allows us to write this to make HTTP calls in a loop and process the results as they arrive :

await foreach(var someValue from someAsyncIterator(5))
{
    ...
}

IAsyncEnumerable<string> someAsyncIterator(int max)
{
    for(int i=0;i<max;i++)
    {
        var response=await httpClient.GetStringAsync($"{baseUrl}/{i}");
        yield return response;
    }
}

When they appear as action results it's only to allow the ASP.NET Core middleware to start processing results as they are produced, they don't affect the contents of the HTTP response itself.

gRPC's streams on the other hand allow the server to send individual responses to the client asynchronously. Laurent Kempe in gRPC and C# 8 Async stream and Steve Gordon in Server Streaming with GRPC and .NET Core show how these can be used together

Copying from Steve Gordon's samples, let's say we have a weather service that sends forecasts to the client, whose proto file contains :

service WeatherForecasts {
  rpc GetWeather (google.protobuf.Empty) returns (WeatherReply);
  rpc GetWeatherStream (google.protobuf.Empty) returns (stream WeatherData);
  rpc GetTownWeatherStream (stream TownWeatherRequest) returns (stream TownWeatherForecast);
}

Before C# 8, the client would have to block until it received all responses before processing them:

using var channel = GrpcChannel.ForAddress("https://localhost:5005");
var client = new WeatherForecastsClient(channel);
var reply = await client.GetWeatherAsync(new Empty());
foreach (var forecast in reply.WeatherData)
{
        //Do something with the data
}

In C# 8 though, the responses can be received and processed as they arrive :

using var replies = client.GetWeatherStream(new Empty(), cancellationToken: cts.Token);

await foreach (var weatherData in replies.ResponseStream.ReadAllAsync(cancellationToken: cts.Token))
{
        //Do something with the data
}

**

Arvillaarvin answered 16/10, 2019 at 12:41 Comment(14)
HttpClient.GetStringAsync returns a Task<string>, so that example doesn't seem right. And the weather example is precisely what's confusing. Weather data streams never end, because weather never ends. There's no such thing as "blocking until it receives all responses" because the end never comes. Before C#8, a client would likely poll the server, or the server would push weather data to the client. Hence my notion that async streams are for "pull based" situations. Am I wrong?Kirby
@AlexDresko await httpClient.GetStringAsync($"{baseUrl}/{i}") produces a string. As for the weather example, it's about this particualr sample code which returns only a few dozen responses with an artificial delay. There is an end to the calls.Arvillaarvin
@AlexDresko just forget you ever heard about async streams and think about await foreach. I have to make an effort myself to call this feature async streams. The term is used because it exists in other languages, not because it has anything to do with IO. In Java for example, streams are push-based iterators when .NET's IEnumerable is pull-based. And now forget about push- vs pull- as it has nothing to do with gRPCArvillaarvin
Your last comment is a better answer than your actual answer which still suffers from the points I brought up in my first comment #58413350. Unfortunately, I cannot reward this answer until it negates the very confusion I came here with.Kirby
@AlexDresko Please explain why you think that the Task return type from HttpClient makes the example wrong.Objectionable
@Objectionable the problem is the async streams term, not the actual example.When people read stream they take it to mean IO, which has nothing to do with await foreach. The C# team picked the term used by Java for its own implementation of push-style IEnumerable. Java called it Streams so the C# team, probably interested in interop in the Android world, also used streams for the new feature. If that sounds strange, the primary motivation for default interface members was interop with the default members used in the Android SDKArvillaarvin
@Objectionable in short what were they thinking when they called this async streams?Arvillaarvin
@Objectionable For one thing, it doesn't even compile. httpClient.GetStringAsync returns a Task<string>, not IAsyncEnumerable<string>. Unless I'm completely stupid, his for loop is the equivalent of await foreach (var strange in Task.FromResult("some string")) {} Also, his for loop doesn't appear to do anything meaningful with i.Kirby
@AlexDresko what code are you talking about? await client.GetStringAsync() returns a single value. It's yield return that returns that value in the iterator immediatelly. As for i, that's part of the URL. I was trying to show the difference between the concepts and show that they have nothing in common, not write a full example, or a tutorial. You've already linked to a very good tutorial, the same one I started withArvillaarvin
@AlexDresko Task.FromResult doesn't run anything either, it returns a completed task. GetStringAsync() on the other hand runs asynchronously and is awaited by await without blocking the caller. That's what async/await is about. In turn the IAsyncEnumerable await for values without blocking. You couldn't do that in the pastArvillaarvin
Oh, I see now, you're right. The confusing thing, then, is that it's a for loop hard coded to return a single value. Not sure why anyone would use that as an example when trying to make something clear.Kirby
@AlexDresko because I was writing an answer for a different question - what's the difference between async streams and gRPC streams, not trying to write a tutorial or even a blog post. Christian Nagel already wrote a very good tutorial on async streamsArvillaarvin
I will accept this answer now since I believe there are enough comments here that someone can infer a complete answer, even if it requires making sense of our back-and-forth. I did run across this answer last night, which I think assists with why/when one would chose async streams over the alternatives. It does seem to suggest that my "pull based" assertion was correct. Thank you for taking time to help me understand all of this. I do understand now, and it was extremely helpful being able to talk it out. We just had to get on the same page.Kirby
When they appear as action results it's only to allow the ASP.NET Core middleware to start processing results as they are produced, they don't affect the contents of the HTTP response itself. This was an extremely useful statement, I hadn't found it clearly expressed anywhere else. +1Embed
P
0

Async streams cause real confusion, but it's just about not blocking the calling code. In my previous project, I saw people try to use it for concurrent async operations, for instance, sending notifications to clients and saying that it is good for performance, but it's not in this case. This is a comparison: Async streams and concurrent asynchronous I/O operations

Progression answered 29/7, 2023 at 16:54 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Kozloski

© 2022 - 2024 — McMap. All rights reserved.