-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Default implementation of Stream.BeginRead/BeginWrite #33447
Comments
I realized I may not have been straightforward in what I am asking for. There are several possible courses of action that I can see:
|
The concern seems valid from a quick peek at the code. It appears to function like this in Framework as well.
It would be a breaking change to the current behavior. I'd be concerned that anything using APM these days is probably very old code in "don't fix what isn't broken" mode and we'd risk destabilizing them. Where has Mono seen the most benefit from making this change?
I like this one better. |
Correct. The duplex stream implementations in .NET Framework also override
Breakage is exactly what happened to us with the linked Mono change. We have some old code from 3rd-party library that depends on I cannot speak for @baulig what was the motivation for the change. It was a code cleanup. Neither him, nor me, realized during the review that the default implementation in |
I think there are multiple things being confused here. Stream.BeginRead/Write have two relevant behaviors that seem to be conflated here:
(1) is behavior inherited from .NET Framework 1.x. I think for .NET 5 it'd be reasonable to change it to wait asynchronously rather than synchronously; there are code paths that already do that, just not BeginRead/Write, so the product change would likely end up being flipping a false to true in each method (though at that point we could probably delete some dead code as well). However, I'd be worried about going beyond that and removing the serialization entirely. It's not unlikely that code today is relying on that internal semaphore (knowingly or unknowingly) to ensure that operations don't overlap. That's true even if they're using ReadAsync/WriteAsync, for which the base implementations inherit the same serialization behavior (albeit asynchronously waiting rather than blocking). Changing (2) would almost certainly break a ton of stuff, lead to stack overflows, infinite loops, etc. Any time a type adds virtual methods when it already has some, those new methods generally need to have base implementations provided in terms of the functionality already there. That establishes an ordering which derived types come to rely on. If we change that order here, and, say, a type was overriding ReadAsync/WriteAsync to just add some logging or something and then delegating to the base method, changing the base Begin/EndXx methods to delegate to the XxAsync methods would likely lead to an infinite loop / stackoverflow.
This is part of the conflation I'm talking about. Even if the base methods were changed to not serialize at all, we'd still want the derived overrides that utilize TaskToApm, because we want the derived Begin/End methods to be implemented in terms of the XxAsync methods.
Override Read/WriteAsync, and stop using the legacy Begin/End pattern. If you're implementing a Stream in a shared library where you think someone might still be calling your Begin/End methods, then if you really care about perf for some reason when using Begin/End, or if you do have one of these duplex streams and need them to be able to run concurrently, reimplement Begin/End in terms of the XxAsync methods. Folks that need that can copy the TaskToApm internal helper into their own code (and it can be simplified even further if perf doesn't matter as much); I'd like not to expose yet more public types purely to service an obsolete model. |
I'd be worried about that too. The question is whether there is a reasonably safe path to do a separate serialization for reads and writes for non-seekable streams. That would likely cover bulk of the cases where the current behavior is problematic.
Shared libraries are my primary concern. I am not overly happy with code being copied over to different places but it's fine as long as it's an official guidance. I just need something to point people to. It is non-obvious that the default implementation can very easily lead to deadlocks. It hit us once due to a change in Mono that looked like a trivial code cleanup. Then the same thing almost happened again when Xamarin was being ported to run on .NET 5. |
Just because a stream is non-seekable doesn't mean it's duplex. We also currently pay for a field on every stream to store the SemaphoreSlim, which is one field more than I'd like, plus actually creating that instance when it's used. I'd very much like to not add yet another one.
Sounds like something good to be added to documentation, and you can point people to that.
Sure, but that non-obviousness wouldn't be addressed by exposing TaskToApm. |
You are right. There are probably no proper criteria for detecting that a particular stream is a duplex stream. Seems like there is no easy implementation fix without changing the API shape.
The issue is to document the non-obviousness and also offer guidance on how to correctly address it for library authors. The current preferred way to address it seems to be to use |
There is an issue with using And as I already pointed out in that other issue, the only guarantee that we've been making in Mono's |
What issue?
I'm not following. What are you arguing for or against? |
The I'm arguing against making changes to Mono's |
This is how the APM pattern works in general: TaskToApm is just reflecting the realities of the pattern. |
… and underlying implementation. Unlike the default implementation in Stream it allows parallel read along with parallel write using the standard TaskToApm pattern (dotnet/runtime#33447). This reverts part of mono#17393 which could lead to deadlocks as reported in mono#18865.
…#19442) * Add BeginRead/BeginWrite/EndRead/EndWrite overloads back to SslStream and underlying implementation. Unlike the default implementation in Stream it allows parallel read along with parallel write using the standard TaskToApm pattern (dotnet/runtime#33447). This reverts part of #17393 which could lead to deadlocks as reported in #18865. * Move the TaskToApm pattern from MobileAuthenticatedStream to SslStream * Bump API snapshot submodule Co-authored-by: monojenkins <[email protected]>
… and underlying implementation. Unlike the default implementation in Stream it allows parallel read along with parallel write using the standard TaskToApm pattern (dotnet/runtime#33447). This reverts part of mono#17393 which could lead to deadlocks as reported in mono#18865.
… SslStream (#19476) * Add BeginRead/BeginWrite/EndRead/EndWrite overloads back to SslStream and underlying implementation. Unlike the default implementation in Stream it allows parallel read along with parallel write using the standard TaskToApm pattern (dotnet/runtime#33447). This reverts part of #17393 which could lead to deadlocks as reported in #18865. * Move the TaskToApm pattern from MobileAuthenticatedStream to SslStream * Bump API snapshot submodule Co-authored-by: Filip Navara <[email protected]>
Hello, |
That is unrelated to this issue. |
Currently the default implementation of
Stream.BeginRead
andBeginWrite
is tailored towards seekable streams. It intentionally blocks parallel requests due to the concern that multiple parallel requests would modify the position in the stream. However, this behavior is problematic for non-seekable duplex streams such as network streams, compression streams, or other wrapper streams with some sort of encoding. These duplex streams should allow at least one of parallel read and write each to facilitate the documented pattern whereBeginRead
is always active to read incoming data.Many of the streams implementations (GzipStream, HttpBaseStream, QuicStream, PipeReaderStream, PipeWriterStream, ReadOnlyMemoryStream, BrotliStream, SslStream; the list goes on) use the same
TaskToApm
pattern to overcome this limitation.The problem with the above approach is the following:
HasOverriddenBeginEndRead
andHasOverriddenBeginEndWrite
).TaskToApm
is not externally available for consumption by other products such as Xamarin (Build bgen and the product assemblies using/referencing netcore3.1. xamarin/xamarin-macios#8070 (comment))I originally discovered this as breakage in Mono (mono/mono#18865) in their implementation of
SslStream
but it keeps popping up in other contexts and I think this should be addressed in a coordinated fashion./cc @baulig @steveisok @rolfbjarne @JeremyKuhne @carlossanlop
The text was updated successfully, but these errors were encountered: