Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit bef7c43

Browse files
committed
Amortize StreamPipeReader ReadAsync calls
1 parent 929bd81 commit bef7c43

File tree

4 files changed

+218
-30
lines changed

4 files changed

+218
-30
lines changed

src/System.IO.Pipelines/ref/System.IO.Pipelines.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Configurations>netstandard-Debug;netstandard-Release</Configurations>
55
<!-- We only plan to use this ref in netcoreapp. For all other netstandard compatible frameworks we should use the lib
66
asset instead. -->
7-
<PackageTargetFramework Condition="'$(TargetGroup)' == 'netstandard'">netcoreapp2.0</PackageTargetFramework>
7+
<PackageTargetFramework Condition="'$(TargetGroup)' == 'netstandard'">netcoreapp3.0</PackageTargetFramework>
88
</PropertyGroup>
99
<ItemGroup>
1010
<Compile Include="System.IO.Pipelines.cs" />
@@ -16,4 +16,10 @@
1616
<ItemGroup>
1717
<ProjectReference Include="..\..\System.Buffers\ref\System.Buffers.csproj" />
1818
</ItemGroup>
19+
<ItemGroup Condition="'$(TargetGroup)'=='netcoreapp'">
20+
<Reference Include="Microsoft.Bcl.AsyncInterfaces" />
21+
</ItemGroup>
22+
<ItemGroup Condition="'$(TargetGroup)'=='netstandard'">
23+
<ProjectReference Include="..\..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj" />
24+
</ItemGroup>
1925
</Project>

src/System.IO.Pipelines/src/System.IO.Pipelines.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<ProjectGuid>{1032D5F6-5AE7-4002-A0E4-FEBEADFEA977}</ProjectGuid>
4-
<Configurations>netcoreapp-Debug;netcoreapp-Debug;netcoreapp-Release;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
4+
<Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
5+
<DefineConstants Condition="'$(TargetsNetFx)' == 'true'">$(DefineConstants);netstandard</DefineConstants>
56
</PropertyGroup>
67
<ItemGroup>
78
<Compile Include="$(CommonPath)\CoreLib\System\Threading\Tasks\TaskToApm.cs">
@@ -57,5 +58,6 @@
5758
<Reference Include="System.Threading.Tasks.Extensions" />
5859
<Reference Include="System.Threading.Tasks" />
5960
<Reference Include="System.Threading.ThreadPool" />
61+
<Reference Include="Microsoft.Bcl.AsyncInterfaces" />
6062
</ItemGroup>
6163
</Project>

src/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeReader.cs

Lines changed: 187 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
using System.Buffers;
66
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
78
using System.Threading;
89
using System.Threading.Tasks;
10+
using System.Threading.Tasks.Sources;
911

1012
namespace System.IO.Pipelines
1113
{
12-
internal class StreamPipeReader : PipeReader
14+
internal class StreamPipeReader : PipeReader, IValueTaskSource<ReadResult>
1315
{
1416
internal const int InitialSegmentPoolSize = 4; // 16K
1517
internal const int MaxSegmentPoolSize = 256; // 1MB
@@ -28,11 +30,19 @@ internal class StreamPipeReader : PipeReader
2830
private BufferSegment _readTail;
2931
private long _bufferedBytes;
3032
private bool _examinedEverything;
31-
private object _lock = new object();
33+
private readonly object _lock = new object();
3234

3335
// Mutable struct! Don't make this readonly
3436
private BufferSegmentStack _bufferSegmentPool;
35-
private bool _leaveOpen;
37+
private readonly bool _leaveOpen;
38+
39+
// State for async reads
40+
private volatile bool _readInProgress;
41+
private readonly Action _onReadCompleted;
42+
private ManualResetValueTaskSourceCore<ReadResult> _readMrvts;
43+
private ValueTaskAwaiter<int> _readAwaiter;
44+
private CancellationToken _readCancellation;
45+
private CancellationTokenRegistration _readRegistration;
3646

3747
/// <summary>
3848
/// Creates a new StreamPipeReader.
@@ -53,6 +63,7 @@ public StreamPipeReader(Stream readingStream, StreamPipeReaderOptions options)
5363
_pool = options.Pool == MemoryPool<byte>.Shared ? null : options.Pool;
5464
_bufferSize = _pool == null ? options.BufferSize : Math.Min(options.BufferSize, _pool.MaxBufferSize);
5565
_leaveOpen = options.LeaveOpen;
66+
_onReadCompleted = new Action(OnReadCompleted);
5667
}
5768

5869
/// <summary>
@@ -72,11 +83,7 @@ private CancellationTokenSource InternalTokenSource
7283
{
7384
lock (_lock)
7485
{
75-
if (_internalTokenSource == null)
76-
{
77-
_internalTokenSource = new CancellationTokenSource();
78-
}
79-
return _internalTokenSource;
86+
return (_internalTokenSource ??= new CancellationTokenSource());
8087
}
8188
}
8289
}
@@ -193,39 +200,59 @@ public override void OnWriterCompleted(Action<Exception, object> callback, objec
193200
}
194201

195202
/// <inheritdoc />
196-
public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
203+
public override ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
197204
{
198-
// TODO ReadyAsync needs to throw if there are overlapping reads.
199-
ThrowIfCompleted();
200-
201-
// PERF: store InternalTokenSource locally to avoid querying it twice (which acquires a lock)
202-
CancellationTokenSource tokenSource = InternalTokenSource;
203-
if (TryReadInternal(tokenSource, out ReadResult readResult))
205+
if (_readInProgress)
204206
{
205-
return readResult;
207+
// Throw if there are overlapping reads; throwing unwrapped as it suggests last read was not awaited
208+
// so we surface it directly rather than wrapped in a Task (as this one will likely also not be awaited).
209+
ThrowConcurrentReadsNotSupported();
206210
}
211+
_readInProgress = true;
207212

208-
if (_isStreamCompleted)
213+
bool isAsync = false;
214+
try
209215
{
210-
return new ReadResult(buffer: default, isCanceled: false, isCompleted: true);
211-
}
212216

213-
var reg = new CancellationTokenRegistration();
214-
if (cancellationToken.CanBeCanceled)
215-
{
216-
reg = cancellationToken.UnsafeRegister(state => ((StreamPipeReader)state).Cancel(), this);
217-
}
217+
ThrowIfCompleted();
218+
219+
// PERF: store InternalTokenSource locally to avoid querying it twice (which acquires a lock)
220+
CancellationTokenSource tokenSource = InternalTokenSource;
221+
if (TryReadInternal(tokenSource, out ReadResult readResult))
222+
{
223+
return new ValueTask<ReadResult>(readResult);
224+
}
225+
226+
if (_isStreamCompleted)
227+
{
228+
return new ValueTask<ReadResult>(new ReadResult(buffer: default, isCanceled: false, isCompleted: true));
229+
}
230+
231+
var reg = new CancellationTokenRegistration();
232+
if (cancellationToken.CanBeCanceled)
233+
{
234+
reg = cancellationToken.UnsafeRegister(state => ((StreamPipeReader)state).Cancel(), this);
235+
}
218236

219-
using (reg)
220-
{
221237
var isCanceled = false;
222238
try
223239
{
224240
AllocateReadTail();
225241

226242
Memory<byte> buffer = _readTail.AvailableMemory.Slice(_readTail.End);
227243

228-
int length = await InnerStream.ReadAsync(buffer, tokenSource.Token).ConfigureAwait(false);
244+
ValueTask<int> resultTask = InnerStream.ReadAsync(buffer, tokenSource.Token);
245+
int length;
246+
if (resultTask.IsCompletedSuccessfully)
247+
{
248+
length = resultTask.Result;
249+
}
250+
else
251+
{
252+
isAsync = true;
253+
// Need to go async
254+
return CompleteReadAsync(resultTask, cancellationToken, reg);
255+
}
229256

230257
Debug.Assert(length + _readTail.End <= _readTail.AvailableMemory.Length);
231258

@@ -252,8 +279,27 @@ public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancella
252279
}
253280

254281
}
282+
finally
283+
{
284+
if (!isAsync)
285+
{
286+
reg.Dispose();
287+
}
288+
}
255289

256-
return new ReadResult(GetCurrentReadOnlySequence(), isCanceled, _isStreamCompleted);
290+
return new ValueTask<ReadResult>(new ReadResult(GetCurrentReadOnlySequence(), isCanceled, _isStreamCompleted));
291+
}
292+
catch (Exception ex)
293+
{
294+
return new ValueTask<ReadResult>(Task.FromException<ReadResult>(ex));
295+
}
296+
finally
297+
{
298+
if (!isAsync)
299+
{
300+
Debug.Assert(_readInProgress);
301+
_readInProgress = false;
302+
}
257303
}
258304
}
259305

@@ -275,6 +321,11 @@ private void ThrowIfCompleted()
275321

276322
public override bool TryRead(out ReadResult result)
277323
{
324+
if (_readInProgress)
325+
{
326+
ThrowConcurrentReadsNotSupported();
327+
}
328+
278329
ThrowIfCompleted();
279330

280331
return TryReadInternal(InternalTokenSource, out result);
@@ -362,5 +413,113 @@ private void Cancel()
362413
{
363414
InternalTokenSource.Cancel();
364415
}
416+
417+
static void ThrowConcurrentReadsNotSupported()
418+
{
419+
throw new InvalidOperationException($"Concurrent reads are not supported; await the {nameof(ValueTask<ReadResult>)} before starting next read.");
420+
}
421+
422+
private ValueTask<ReadResult> CompleteReadAsync(ValueTask<int> task, CancellationToken cancellationToken, CancellationTokenRegistration reg)
423+
{
424+
Debug.Assert(_readInProgress, "Read not in progress");
425+
426+
_readCancellation = cancellationToken;
427+
_readRegistration = reg;
428+
429+
_readAwaiter = task.GetAwaiter();
430+
431+
return new ValueTask<ReadResult>(this, _readMrvts.Version);
432+
}
433+
434+
private void OnReadCompleted()
435+
{
436+
try
437+
{
438+
int length = _readAwaiter.GetResult();
439+
440+
Debug.Assert(length + _readTail.End <= _readTail.AvailableMemory.Length);
441+
442+
_readTail.End += length;
443+
_bufferedBytes += length;
444+
445+
if (length == 0)
446+
{
447+
_isStreamCompleted = true;
448+
}
449+
450+
_readMrvts.SetResult(new ReadResult(GetCurrentReadOnlySequence(), isCanceled: false, _isStreamCompleted));
451+
}
452+
catch (OperationCanceledException oce)
453+
{
454+
// Get the source before clearing (and replacing)
455+
CancellationTokenSource tokenSource = InternalTokenSource;
456+
ClearCancellationToken();
457+
if (tokenSource.IsCancellationRequested && !_readCancellation.IsCancellationRequested)
458+
{
459+
// Catch cancellation and translate it into setting isCanceled = true
460+
_readMrvts.SetResult(new ReadResult(GetCurrentReadOnlySequence(), isCanceled: true, _isStreamCompleted));
461+
}
462+
else
463+
{
464+
_readMrvts.SetException(oce);
465+
}
466+
}
467+
catch (Exception ex)
468+
{
469+
_readMrvts.SetException(ex);
470+
}
471+
finally
472+
{
473+
_readRegistration.Dispose();
474+
_readRegistration = default;
475+
}
476+
}
477+
478+
ReadResult IValueTaskSource<ReadResult>.GetResult(short token)
479+
{
480+
ValidateReading();
481+
ReadResult result = _readMrvts.GetResult(token);
482+
483+
_readCancellation = default;
484+
_readAwaiter = default;
485+
_readMrvts.Reset();
486+
487+
Debug.Assert(_readInProgress);
488+
_readInProgress = false;
489+
490+
return result;
491+
}
492+
493+
ValueTaskSourceStatus IValueTaskSource<ReadResult>.GetStatus(short token)
494+
=> _readMrvts.GetStatus(token);
495+
496+
void IValueTaskSource<ReadResult>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
497+
{
498+
ValidateReading();
499+
_readMrvts.OnCompleted(continuation, state, token, flags);
500+
501+
if ((flags & ValueTaskSourceOnCompletedFlags.FlowExecutionContext) != 0)
502+
{
503+
_readAwaiter.OnCompleted(_onReadCompleted);
504+
}
505+
else
506+
{
507+
_readAwaiter.UnsafeOnCompleted(_onReadCompleted);
508+
}
509+
}
510+
511+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
512+
private void ValidateReading()
513+
{
514+
if (!_readInProgress)
515+
{
516+
ThrowReadNotInProgress();
517+
}
518+
519+
static void ThrowReadNotInProgress()
520+
{
521+
throw new InvalidOperationException("Read not in progress");
522+
}
523+
}
365524
}
366525
}

src/System.IO.Pipelines/tests/StreamPipeReaderTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ async Task DoAsyncWrites(PipeWriter writer, int[] bufferSizes)
120120
pipe.Reader.Complete();
121121
}
122122

123+
[Fact]
124+
public void ConcurrentReadsThrow()
125+
{
126+
var pipe = new Pipe();
127+
var options = new StreamPipeReaderOptions(bufferSize: 4096);
128+
PipeReader reader = PipeReader.Create(pipe.Reader.AsStream(), options);
129+
130+
ValueTask<ReadResult> valueTask = reader.ReadAsync();
131+
132+
Assert.False(valueTask.IsCompleted);
133+
134+
Assert.Throws<InvalidOperationException>(() => reader.ReadAsync());
135+
Assert.Throws<InvalidOperationException>(() => reader.TryRead(out _));
136+
137+
Assert.False(valueTask.IsCompleted);
138+
139+
reader.Complete();
140+
141+
pipe.Reader.Complete();
142+
}
143+
123144
[Theory]
124145
[MemberData(nameof(ReadSettings))]
125146
public async Task ReadWithDifferentSettings(int bytesInBuffer, int bufferSize, int minimumReadSize, int[] readBufferSizes)

0 commit comments

Comments
 (0)