-
Notifications
You must be signed in to change notification settings - Fork 524
Use ValueTask instead of custom awaiter for SocketAwaitableEventArgs #2998
Conversation
- This could improve the performance of async operations (reduce the number of allocations)
protected override void OnCompleted(SocketAsyncEventArgs _) | ||
/// <summary>Initiates a send operation on the associated socket.</summary> | ||
/// <returns>This instance.</returns> | ||
public ValueTask<int> SendAsync(Socket socket) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code for ReceiveAsync
and SendAsync
is quite similar. Can you refactor this to one method, to avoid the code-duplication?
@dotnet-bot test OSX 10.12 Debug Build |
{ | ||
ThrowSocketException(SocketError); | ||
Debug.Fail("Multiple continuations registered!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be really hard to find this bug in release build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats why we run tests in both. I'm not worried about this though, this isn't general purpose. This is specifically used in a single place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I see no reason to not just throw or even Environment.FailFast in this case.
{ | ||
OnCompleted(this); | ||
Volatile.Write(ref _completion, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I guess theoretically without this volatile write, if OnCompleted(SocketAsyncEventArgs)
was called from another thread before OnCompleted(Action<object> continuation, ...
, the first OnCompleted call could observe a stale _completion callback and invoke it a second time.
{ | ||
Action<object> completion = _completion; | ||
|
||
if (completion != null || (completion = Interlocked.CompareExchange(ref _completion, _callbackCompleted, null)) != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure completion != null
is safe here. I think you need a memory barrier here for the same reason you need it in Reset(). For one, without a memory barrier, reading UserToken could be reordered to before reading _completion. There's also the issue with not transitioning into the _callbackCompleted state which would make GetStatus wrong momentarily, but I don't think that's very important.
|
||
private readonly PipeScheduler _ioScheduler; | ||
|
||
private Action _callback; | ||
private Action<object> _completion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think _callback or _continuation is a better name
If this turns out to be valuable, OutputFlowControlAwaitable, ThreadPoolAwaitable and even LibuvAwaitable could probably use this IValueTaskSource treatment. As this gets more complicated, I'm wondering if at least some of this logic could be shared. |
} | ||
|
||
protected override void OnCompleted(SocketAsyncEventArgs _) | ||
private ValueTask<int> CompleteSocketOperation() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer GetCompletedValueTask() since this isn't used to complete socket operations in the async case.
if (ReferenceEquals(awaitableState, _callbackCompleted)) | ||
{ | ||
#if NETCOREAPP2_1 | ||
ThreadPool.QueueUserWorkItem(continuation, state, preferLocal: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get this should be rare, but why not UQUWI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The continuation is Action<object>
, so it's not like we're boxing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, you're right, I could just queue this
instead of using the state object directly since the UserToken is a field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use the UserToken field at all in this case? Just use the state object directly. UserToken is only necessary if OnCompleted(SocketAsyncEventArgs
is responsible to running the continuation.
ThreadPool.QueueUserWorkItem(continuation, state, preferLocal: false); | ||
#else | ||
Task.Factory.StartNew(continuation, state); | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you null out UserToken in OnCompleted(SocketAsyncEventArgs
it's probably best to also do that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll null it out after the work item is invoked but before calling the continuation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to wait until after the work item invoked to null out UserToken for the same reason you don't need to in OnCompleted(SocketAsyncEventArgs
. Just use the local state reference which isn't null. Technically you don't need to even set UserToken in this case, you could just set it in an else block at the end.
{ | ||
_ioScheduler.Schedule(state => ((Action)state)(), continuation); | ||
} | ||
Reset(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why call Reset() here? _completion must already be null, right? Which now that I think about it means GetStatus is wrong after sync completions. Again, I'm not sure how important that is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sync completions don't use this code path. Unless I'm missing something...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
??? This is only called when ReceiveAsync/SendAsync return false, right?
I guess it's confusing because of the name. That's why I recommend renaming this to GetCompletedValueTask().
var callbackState = args.UserToken; | ||
args.UserToken = null; | ||
var callback = args._callback; | ||
callback(callbackState); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what was up now. I always seem to forget that you can't convert an Action<object>
to a WaitCallback
without allocating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to change this back, it's a tad bit complicated and not worth saving the allocation.
|
||
if (SocketError != SocketError.Success) | ||
public int GetResult(short token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The active test run was aborted. Reason: Unhandled Exception: System.InvalidOperationException: Multiple continuations registered!
You still need to null out _callback
here. It's GetCompletedValueTask() where nulling out _callback
is unnecessary because it will always be null there. Assuming you null it out here that is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬 ooops
int bytesTransferred = BytesTransferred; | ||
SocketError error = SocketError; | ||
|
||
Volatile.Write(ref _callback, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_callback should always already be null here.
Both Jenkins Windows failures look really scary. All sockets functional tests and logs like the following:
That's the "you unrooted the request processing continuation and we know b/c of our weak reference" log. In one of the runs, this was logged 7 times. 😱 |
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted); | ||
protected override void OnCompleted(SocketAsyncEventArgs _) | ||
{ | ||
var callback = Interlocked.CompareExchange(ref _callback, _callbackCompleted, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you change this from Interlocked.Exchange to Interlocked.CompareExchange? You want to move into the _callbackCompleted state no matter what. Here you're only transitioning it if a callback isn't registered yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was always using CompareExchange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it wasn't.
KestrelHttpServer/src/Kestrel.Transport.Sockets/Internal/SocketAwaitableEventArgs.cs
Lines 68 to 70 in 081cef0
protected override void OnCompleted(SocketAsyncEventArgs _) | |
{ | |
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted); |
I think the multiple OnCompleted methods make it easy to mix this kind of stuff up.
🆙 📅 |
Looks slightly worse:
VS
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we don't take it if there's no performance improvement and possibly even a regression. It doesn't seem to really simplify things either.
Yes that's the current conclusion. I need to see if there are debugging benefits here before I close it out (also future improvements that might be coming e.g. https://github.com/dotnet/coreclr/issues/20200). |
TODO: Measure performance, I may combine this with #2974