dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.01k stars 4.67k forks source link

Question: Are ChannelWriter<T>s fair? #1009

Closed AndreasHeisel closed 4 years ago

AndreasHeisel commented 4 years ago

Supposed I have a bounded Channel that is fed by multiple producers. This channel blocks because it is full. Producers call WriteAsync multiple times without awaiting it.

Will these writes be executed in order if the channel gets drained?

Clockwork-Muse commented 4 years ago

Define "in order": if you have multiple producers, even if the channel executes the writes in the exact same order as WriteAsync was called, this isn't necessarily the same order as you're expecting. The reality is much more complicated, of course, and prone to wacky timing issues.

AndreasHeisel commented 4 years ago

@Clockwork-Muse I meant chronologically. This would (at least) guarantee that the calls of one producer will be executed in the exact order.
I know that there is some uncertainty if two producers call 'at the same time' due to thread scheduling and multicore issues. But apart from that?

MarcoRossignoli commented 4 years ago

Producers call WriteAsync multiple times without awaiting it.

Mmm it's not clear this statement if channel is bounded and full WritaAsync will await

From Stephen's post https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/

However, ChannelWriter<T> also provides the WriteAsync method; in such a case where the channel is full and writing would need to wait (often referred to as “back pressure”), 
WriteAsync can be used, with the producer awaiting the result of WriteAsync and only being allowed to continue when room becomes available.
AndreasHeisel commented 4 years ago

@MarcoRossignoli Thanks for the link. I see the problem, but it is still interessting to know what would happen if one violates this.

MarcoRossignoli commented 4 years ago

if one violates this

What do you mean for violates?Producers are awaiting(light block...no thread if truly async) and consumer is draining items and signal producers when there is free space, so from the perspective of single producer item are ordered, but you don't know(and you cannot do assumptions) respect other producers.

AndreasHeisel commented 4 years ago

with the producer awaiting the result of WriteAsync and only being allowed to continue when room becomes available

Calling WriteAsync "fire and forget" without awaiting the result.

MarcoRossignoli commented 4 years ago

Ah understood if you don't await "continuation" won't be setup so you don't have guarantee that not overlap with other WriteAsync(on the same path) so you're not violating nothing from channel perspective is your code that declare this behaviour, you "forget" to await the result, you won't be sure in that case about ordering on that path(for path I'm talking about for instance a WriteAsync inside a foreach of some items in a collection).

MarcoRossignoli commented 4 years ago

BTW cc: @stephentoub that works on this and can confirm or reject my thought.

AndreasHeisel commented 4 years ago

Exactly. I have a bounded channel because I need backpressure to prevent the system blowing up. In most cases this ist fine. But in one code path (a sync to async boundary) I can't afford to wait and also can't afford to loose the information. In this very rare situation it is better to exceed the boundary. The idea is, if the WriteAsync calls would queue up to use this as "emergency queue"

MarcoRossignoli commented 4 years ago

You could use a new bounded/unbounded sync write channel to offload item of this particular sync path and correctly await ReadAsync/WriteAsync on consumer that write on "main" bounded channel, you should keep the order in this way. It's a basic TPL Dataflow scenario.

AndreasHeisel commented 4 years ago

I know, but this doubles the memory footprint for a rare szenario. It's also difficult to maintain order between the entries in the unbounded channel an the ones directly insterted into the bounded channel.

Maybe a new feature: ChannelWriter.WriteForced(T item) that ignore bounds :-)

Drawaes commented 4 years ago

Make it unbounded and use a senaphoreslim on the async path if you are really stuck. Making a bounded channel able to be unbounded sometimes means you lose some of the optimisations you can do in theory around the internal data structures.

scalablecory commented 4 years ago

If you call WriteAsync in sequence, each call will complete in-order. If you call WriteAsync concurrently, the order between the concurrent calls will not be defined (it depends on who gets a lock first).

However: if you're doing a fire-and-forget WriteAsync, you're essentially just using WriteAsync as its own extra queue. You've already computed the value, so using an unbounded queue will be far more efficient. If you don't need to guarantee the global ordering, consider feeding an unbounded "emergency queue" into a your normal bounded queue.

AndreasHeisel commented 4 years ago

@scalablecory That is exactly what I tried to archieve. Just wanted to prove it.