From 1a75dbe3d081c42fc5b137857ff0ec7c1c78b9c8 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 14 Jul 2026 12:33:37 +0200 Subject: [PATCH 1/3] Avoid materializing Task objects in StreamReader `StreamReader` implements a diagnostic that throws when a user tries to start multiple simultaneous read. This is not a thread safety guarantee, but rather a best-effort diagnostic. However, it causes many StreamReader APIs to force task objects to be allocated and hurts runtime async performance. This PR switches to a try-finally inside the callees instead to implement the diagnostic and to avoid forcibly allocating the Task objects. --- .../src/System/IO/StreamReader.cs | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index 319d31ddaeed55..e82404f4ae071f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -72,18 +72,36 @@ public class StreamReader : TextReader // We don't guarantee thread safety on StreamReader, but we should at // least prevent users from trying to read anything while an Async // read from the same thread is in progress. - private Task _asyncReadTask = Task.CompletedTask; + private bool _asyncIOInProgress; private void CheckAsyncTaskInProgress() { - // We are not locking the access to _asyncReadTask because this is not meant to guarantee thread safety. + // We are not locking this access because this is not meant to guarantee thread safety. // We are simply trying to deter calling any Read APIs while an async Read from the same thread is in progress. - if (!_asyncReadTask.IsCompleted) + if (_asyncIOInProgress) { ThrowAsyncIOInProgress(); } } + private ThrowOnReadsScope GuardAgainstReads() + { + return new ThrowOnReadsScope(this); + } + + private readonly struct ThrowOnReadsScope : IDisposable + { + private readonly StreamReader _reader; + + public ThrowOnReadsScope(StreamReader reader) + { + _reader = reader; + _reader._asyncIOInProgress = true; + } + + public void Dispose() => _reader._asyncIOInProgress = false; + } + [DoesNotReturn] private static void ThrowAsyncIOInProgress() => throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress); @@ -898,14 +916,13 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = ReadLineAsyncInternal(cancellationToken); - _asyncReadTask = task; - - return new ValueTask(task); + return new ValueTask(ReadLineAsyncInternal(cancellationToken)); } private async Task ReadLineAsyncInternal(CancellationToken cancellationToken) { + using ThrowOnReadsScope _ = GuardAgainstReads(); + if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) { return null; @@ -1026,14 +1043,13 @@ public override Task ReadToEndAsync(CancellationToken cancellationToken) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = ReadToEndAsyncInternal(cancellationToken); - _asyncReadTask = task; - - return task; + return ReadToEndAsyncInternal(cancellationToken); } private async Task ReadToEndAsyncInternal(CancellationToken cancellationToken) { + using ThrowOnReadsScope _ = GuardAgainstReads(); + // Call ReadBuffer, then pull data out of charBuffer. StringBuilder sb = new StringBuilder(_charLen - _charPos); do @@ -1070,10 +1086,7 @@ public override Task ReadAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = ReadAsyncInternal(new Memory(buffer, index, count), CancellationToken.None).AsTask(); - _asyncReadTask = task; - - return task; + return ReadAsyncInternalWithGuard(new Memory(buffer, index, count), CancellationToken.None); } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -1095,6 +1108,12 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ReadAsyncInternal(buffer, cancellationToken); } + private async Task ReadAsyncInternalWithGuard(Memory buffer, CancellationToken cancellationToken) + { + using ThrowOnReadsScope _ = GuardAgainstReads(); + return await ReadAsyncInternal(buffer, cancellationToken).ConfigureAwait(false); + } + internal override async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) { if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) @@ -1281,10 +1300,13 @@ public override Task ReadBlockAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = base.ReadBlockAsync(buffer, index, count); - _asyncReadTask = task; + return ReadBlockAsyncWithGuard(buffer, index, count); + } - return task; + private async Task ReadBlockAsyncWithGuard(char[] buffer, int index, int count) + { + using ThrowOnReadsScope _ = GuardAgainstReads(); + return await base.ReadBlockAsync(buffer, index, count).ConfigureAwait(false); } public override ValueTask ReadBlockAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -1304,15 +1326,13 @@ public override ValueTask ReadBlockAsync(Memory buffer, CancellationT return ValueTask.FromCanceled(cancellationToken); } - ValueTask vt = ReadBlockAsyncInternal(buffer, cancellationToken); - if (vt.IsCompletedSuccessfully) - { - return vt; - } + return ReadBlockAsyncInternalWithGuard(buffer, cancellationToken); + } - Task t = vt.AsTask(); - _asyncReadTask = t; - return new ValueTask(t); + private async ValueTask ReadBlockAsyncInternalWithGuard(Memory buffer, CancellationToken token) + { + using ThrowOnReadsScope _ = GuardAgainstReads(); + return await ReadBlockAsyncInternal(buffer, token).ConfigureAwait(false); } private async ValueTask ReadBufferAsync(CancellationToken cancellationToken) From 76fb30e402575973e73c32c168fffe157efa9ded Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 14 Jul 2026 16:40:35 +0200 Subject: [PATCH 2/3] Clean up in StreamReader, give StreamWriter the same treatment --- .../src/System/IO/StreamReader.cs | 48 +++++------ .../src/System/IO/StreamWriter.cs | 82 ++++++++++--------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index e82404f4ae071f..2d3b752784611b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -84,7 +84,11 @@ private void CheckAsyncTaskInProgress() } } - private ThrowOnReadsScope GuardAgainstReads() + [DoesNotReturn] + private static void ThrowAsyncIOInProgress() => + throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress); + + private ThrowOnReadsScope GuardAgainstOtherReads() { return new ThrowOnReadsScope(this); } @@ -95,17 +99,13 @@ private ThrowOnReadsScope GuardAgainstReads() public ThrowOnReadsScope(StreamReader reader) { + reader._asyncIOInProgress = true; _reader = reader; - _reader._asyncIOInProgress = true; } public void Dispose() => _reader._asyncIOInProgress = false; } - [DoesNotReturn] - private static void ThrowAsyncIOInProgress() => - throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress); - // StreamReader by default will ignore illegal UTF8 characters. We don't want to // throw here because we want to be able to read ill-formed data without choking. // The high level goal is to be tolerant of encoding errors when we read and very strict @@ -921,7 +921,7 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) private async Task ReadLineAsyncInternal(CancellationToken cancellationToken) { - using ThrowOnReadsScope _ = GuardAgainstReads(); + using ThrowOnReadsScope _ = GuardAgainstOtherReads(); if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) { @@ -1048,7 +1048,7 @@ public override Task ReadToEndAsync(CancellationToken cancellationToken) private async Task ReadToEndAsyncInternal(CancellationToken cancellationToken) { - using ThrowOnReadsScope _ = GuardAgainstReads(); + using ThrowOnReadsScope _ = GuardAgainstOtherReads(); // Call ReadBuffer, then pull data out of charBuffer. StringBuilder sb = new StringBuilder(_charLen - _charPos); @@ -1087,6 +1087,12 @@ public override Task ReadAsync(char[] buffer, int index, int count) CheckAsyncTaskInProgress(); return ReadAsyncInternalWithGuard(new Memory(buffer, index, count), CancellationToken.None); + + async Task ReadAsyncInternalWithGuard(Memory buffer, CancellationToken cancellationToken) + { + using ThrowOnReadsScope _ = GuardAgainstOtherReads(); + return await ReadAsyncInternal(buffer, cancellationToken).ConfigureAwait(false); + } } public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -1108,12 +1114,6 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ReadAsyncInternal(buffer, cancellationToken); } - private async Task ReadAsyncInternalWithGuard(Memory buffer, CancellationToken cancellationToken) - { - using ThrowOnReadsScope _ = GuardAgainstReads(); - return await ReadAsyncInternal(buffer, cancellationToken).ConfigureAwait(false); - } - internal override async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) { if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) @@ -1301,12 +1301,12 @@ public override Task ReadBlockAsync(char[] buffer, int index, int count) CheckAsyncTaskInProgress(); return ReadBlockAsyncWithGuard(buffer, index, count); - } - private async Task ReadBlockAsyncWithGuard(char[] buffer, int index, int count) - { - using ThrowOnReadsScope _ = GuardAgainstReads(); - return await base.ReadBlockAsync(buffer, index, count).ConfigureAwait(false); + async Task ReadBlockAsyncWithGuard(char[] buffer, int index, int count) + { + using ThrowOnReadsScope _ = GuardAgainstOtherReads(); + return await base.ReadBlockAsync(buffer, index, count).ConfigureAwait(false); + } } public override ValueTask ReadBlockAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -1327,12 +1327,12 @@ public override ValueTask ReadBlockAsync(Memory buffer, CancellationT } return ReadBlockAsyncInternalWithGuard(buffer, cancellationToken); - } - private async ValueTask ReadBlockAsyncInternalWithGuard(Memory buffer, CancellationToken token) - { - using ThrowOnReadsScope _ = GuardAgainstReads(); - return await ReadBlockAsyncInternal(buffer, token).ConfigureAwait(false); + async ValueTask ReadBlockAsyncInternalWithGuard(Memory buffer, CancellationToken token) + { + using ThrowOnReadsScope _ = GuardAgainstOtherReads(); + return await ReadBlockAsyncInternal(buffer, token).ConfigureAwait(false); + } } private async ValueTask ReadBufferAsync(CancellationToken cancellationToken) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 9608f6bf470007..4435d128a57e5d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -44,13 +44,13 @@ public class StreamWriter : TextWriter // We don't guarantee thread safety on StreamWriter, but we should at // least prevent users from trying to write anything while an Async // write from the same thread is in progress. - private Task _asyncWriteTask = Task.CompletedTask; + private bool _asyncIOInProgress; private void CheckAsyncTaskInProgress() { - // We are not locking the access to _asyncWriteTask because this is not meant to guarantee thread safety. + // We are not locking this access because this is not meant to guarantee thread safety. // We are simply trying to deter calling any Write APIs while an async Write from the same thread is in progress. - if (!_asyncWriteTask.IsCompleted) + if (_asyncIOInProgress) { ThrowAsyncIOInProgress(); } @@ -60,6 +60,24 @@ private void CheckAsyncTaskInProgress() private static void ThrowAsyncIOInProgress() => throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress); + private ThrowOnWritesScope GuardAgainstOtherWrites() + { + return new ThrowOnWritesScope(this); + } + + private readonly struct ThrowOnWritesScope : IDisposable + { + private readonly StreamWriter _writer; + + public ThrowOnWritesScope(StreamWriter writer) + { + writer._asyncIOInProgress = true; + _writer = writer; + } + + public void Dispose() => _writer._asyncIOInProgress = false; + } + // The high level goal is to be tolerant of encoding errors when we read and very strict // when we write. Hence, default StreamWriter encoding will throw on encoding error. // Note: when StreamWriter throws on invalid encoding chars (for ex, high surrogate character @@ -664,14 +682,13 @@ public override Task WriteAsync(char value) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(value, appendNewLine: false); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(value, appendNewLine: false); } private async Task WriteAsyncInternal(char value, bool appendNewLine) { + using ThrowOnWritesScope _ = GuardAgainstOtherWrites(); + if (_charPos == _charLen) { await FlushAsyncInternal(flushStream: false, flushEncoder: false).ConfigureAwait(false); @@ -714,10 +731,7 @@ public override Task WriteAsync(string? value) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(value.AsMemory(), appendNewLine: false, default); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(value.AsMemory(), appendNewLine: false, default); } else { @@ -748,10 +762,7 @@ public override Task WriteAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(new ReadOnlyMemory(buffer, index, count), appendNewLine: false, cancellationToken: default); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(new ReadOnlyMemory(buffer, index, count), appendNewLine: false, cancellationToken: default); } public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) @@ -770,13 +781,13 @@ public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken c return Task.FromCanceled(cancellationToken); } - Task task = WriteAsyncInternal(buffer, appendNewLine: false, cancellationToken: cancellationToken); - _asyncWriteTask = task; - return task; + return WriteAsyncInternal(buffer, appendNewLine: false, cancellationToken: cancellationToken); } private async Task WriteAsyncInternal(ReadOnlyMemory source, bool appendNewLine, CancellationToken cancellationToken) { + using ThrowOnWritesScope _ = GuardAgainstOtherWrites(); + int copied = 0; while (copied < source.Length) { @@ -826,10 +837,7 @@ public override Task WriteLineAsync() ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(ReadOnlyMemory.Empty, appendNewLine: true, cancellationToken: default); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(ReadOnlyMemory.Empty, appendNewLine: true, cancellationToken: default); } public override Task WriteLineAsync(char value) @@ -846,10 +854,7 @@ public override Task WriteLineAsync(char value) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(value, appendNewLine: true); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(value, appendNewLine: true); } public override Task WriteLineAsync(string? value) @@ -871,10 +876,7 @@ public override Task WriteLineAsync(string? value) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(value.AsMemory(), appendNewLine: true, default); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(value.AsMemory(), appendNewLine: true, default); } public override Task WriteLineAsync(char[] buffer, int index, int count) @@ -900,10 +902,7 @@ public override Task WriteLineAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = WriteAsyncInternal(new ReadOnlyMemory(buffer, index, count), appendNewLine: true, cancellationToken: default); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(new ReadOnlyMemory(buffer, index, count), appendNewLine: true, cancellationToken: default); } public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) @@ -921,10 +920,7 @@ public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationTok return Task.FromCanceled(cancellationToken); } - Task task = WriteAsyncInternal(buffer, appendNewLine: true, cancellationToken: cancellationToken); - _asyncWriteTask = task; - - return task; + return WriteAsyncInternal(buffer, appendNewLine: true, cancellationToken: cancellationToken); } public override Task FlushAsync() @@ -936,7 +932,7 @@ public override Task FlushAsync() ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return (_asyncWriteTask = FlushAsyncInternal(flushStream: true, flushEncoder: true, CancellationToken.None)); + return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, CancellationToken.None); } /// Clears all buffers for this stream asynchronously and causes any buffered data to be written to the underlying device. @@ -956,7 +952,13 @@ public override Task FlushAsync(CancellationToken cancellationToken) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return (_asyncWriteTask = FlushAsyncInternal(flushStream: true, flushEncoder: true, cancellationToken)); + return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, cancellationToken); + } + + private async Task FlushAsyncInternalWithGuard(bool flushStream, bool flushEncoder, CancellationToken cancellationToken) + { + using ThrowOnWritesScope _ = GuardAgainstOtherWrites(); + await FlushAsyncInternal(flushStream, flushEncoder, cancellationToken).ConfigureAwait(false); } private Task FlushAsyncInternal(bool flushStream, bool flushEncoder, CancellationToken cancellationToken = default) From f64e6ef660a5406196ace7960851cce705ef97bc Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 17 Jul 2026 19:00:36 +0200 Subject: [PATCH 3/3] Use intrinsic --- .../src/System/IO/StreamReader.cs | 46 +++++++++++++++++-- .../src/System/IO/StreamWriter.cs | 28 +++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index 2d3b752784611b..a835e0b8d8276e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -71,14 +72,22 @@ public class StreamReader : TextReader // We don't guarantee thread safety on StreamReader, but we should at // least prevent users from trying to read anything while an Async - // read from the same thread is in progress. + // read from the same thread is in progress. We track this with the + // following fields. + // + // Generally we prefer to use the bool _asyncIOInProgress, but in + // certain cases for async1 this would require introducing a wrapper + // state machine, and in those cases we use the Task _asyncReadTask + // instead. + // private bool _asyncIOInProgress; + private Task _asyncReadTask = Task.CompletedTask; private void CheckAsyncTaskInProgress() { // We are not locking this access because this is not meant to guarantee thread safety. // We are simply trying to deter calling any Read APIs while an async Read from the same thread is in progress. - if (_asyncIOInProgress) + if (_asyncIOInProgress || !_asyncReadTask.IsCompleted) { ThrowAsyncIOInProgress(); } @@ -1086,7 +1095,14 @@ public override Task ReadAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return ReadAsyncInternalWithGuard(new Memory(buffer, index, count), CancellationToken.None); + if (RuntimeHelpers.IsRuntimeAsync()) + { + return ReadAsyncInternalWithGuard(new Memory(buffer, index, count), CancellationToken.None); + } + + Task task = ReadAsyncInternal(new Memory(buffer, index, count), CancellationToken.None).AsTask(); + _asyncReadTask = task; + return task; async Task ReadAsyncInternalWithGuard(Memory buffer, CancellationToken cancellationToken) { @@ -1300,7 +1316,14 @@ public override Task ReadBlockAsync(char[] buffer, int index, int count) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return ReadBlockAsyncWithGuard(buffer, index, count); + if (RuntimeHelpers.IsRuntimeAsync()) + { + return ReadBlockAsyncWithGuard(buffer, index, count); + } + + Task task = base.ReadBlockAsync(buffer, index, count); + _asyncReadTask = task; + return task; async Task ReadBlockAsyncWithGuard(char[] buffer, int index, int count) { @@ -1326,7 +1349,20 @@ public override ValueTask ReadBlockAsync(Memory buffer, CancellationT return ValueTask.FromCanceled(cancellationToken); } - return ReadBlockAsyncInternalWithGuard(buffer, cancellationToken); + if (RuntimeHelpers.IsRuntimeAsync()) + { + return ReadBlockAsyncInternalWithGuard(buffer, cancellationToken); + } + + ValueTask vt = ReadBlockAsyncInternal(buffer, cancellationToken); + if (vt.IsCompletedSuccessfully) + { + return vt; + } + + Task t = vt.AsTask(); + _asyncReadTask = t; + return new ValueTask(t); async ValueTask ReadBlockAsyncInternalWithGuard(Memory buffer, CancellationToken token) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index 4435d128a57e5d..ce5dee0c97ffaa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -43,14 +43,22 @@ public class StreamWriter : TextWriter // We don't guarantee thread safety on StreamWriter, but we should at // least prevent users from trying to write anything while an Async - // write from the same thread is in progress. + // write from the same thread is in progress. We track this with the + // following fields. + // + // Generally we prefer to use the bool _asyncIOInProgress, but in + // certain cases for async1 this would require introducing a wrapper + // state machine, and in those cases we use Task _asyncWriteTask + // instead. + // private bool _asyncIOInProgress; + private Task _asyncWriteTask = Task.CompletedTask; private void CheckAsyncTaskInProgress() { // We are not locking this access because this is not meant to guarantee thread safety. // We are simply trying to deter calling any Write APIs while an async Write from the same thread is in progress. - if (_asyncIOInProgress) + if (_asyncIOInProgress || !_asyncWriteTask.IsCompleted) { ThrowAsyncIOInProgress(); } @@ -932,7 +940,13 @@ public override Task FlushAsync() ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, CancellationToken.None); + + if (RuntimeHelpers.IsRuntimeAsync()) + { + return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, CancellationToken.None); + } + + return (_asyncWriteTask = FlushAsyncInternal(flushStream: true, flushEncoder: true, CancellationToken.None)); } /// Clears all buffers for this stream asynchronously and causes any buffered data to be written to the underlying device. @@ -952,7 +966,13 @@ public override Task FlushAsync(CancellationToken cancellationToken) ThrowIfDisposed(); CheckAsyncTaskInProgress(); - return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, cancellationToken); + + if (RuntimeHelpers.IsRuntimeAsync()) + { + return FlushAsyncInternalWithGuard(flushStream: true, flushEncoder: true, cancellationToken); + } + + return (_asyncWriteTask = FlushAsyncInternal(flushStream: true, flushEncoder: true, cancellationToken)); } private async Task FlushAsyncInternalWithGuard(bool flushStream, bool flushEncoder, CancellationToken cancellationToken)