diff --git a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj
index 951011f5416dff..540338764f3c5f 100644
--- a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj
+++ b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj
@@ -212,6 +212,8 @@
Link="Common\System\Net\SocketProtocolSupportPal.Unix" />
+
_handle.PreferInlineCompletions;
+ set => _handle.PreferInlineCompletions = value;
+ }
+
partial void ValidateForMultiConnect(bool isMultiEndpoint)
{
// ValidateForMultiConnect is called before any {Begin}Connect{Async} call,
diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
index 301cd41bbc40ad..3a85246ee0e010 100644
--- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
+++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
@@ -843,7 +843,7 @@ public bool StartAsyncOperation(SocketAsyncContext context, TOperation operation
}
}
- public AsyncOperation? ProcessSyncEventOrGetAsyncEvent(SocketAsyncContext context, bool skipAsyncEvents = false)
+ public AsyncOperation? ProcessSyncEventOrGetAsyncEvent(SocketAsyncContext context, bool skipAsyncEvents = false, bool processAsyncEvents = true)
{
AsyncOperation op;
using (Lock())
@@ -864,6 +864,7 @@ public bool StartAsyncOperation(SocketAsyncContext context, TOperation operation
Debug.Assert(_isNextOperationSynchronous == (op.Event != null));
if (skipAsyncEvents && !_isNextOperationSynchronous)
{
+ Debug.Assert(!processAsyncEvents);
// Return the operation to indicate that the async operation was not processed, without making
// any state changes because async operations are being skipped
return op;
@@ -901,6 +902,11 @@ public bool StartAsyncOperation(SocketAsyncContext context, TOperation operation
{
// Async operation. The caller will figure out how to process the IO.
Debug.Assert(!skipAsyncEvents);
+ if (processAsyncEvents)
+ {
+ op.Process();
+ return null;
+ }
return op;
}
}
@@ -1190,6 +1196,14 @@ public SocketAsyncContext(SafeSocketHandle socket)
_sendQueue.Init();
}
+ public bool PreferInlineCompletions
+ {
+ // Socket.PreferInlineCompletions is an experimental API with internal access modifier.
+ // PreserveDependency ensures the setter is available externally using reflection.
+ [PreserveDependency("set_PreferInlineCompletions", "System.Net.Sockets.Socket")]
+ get => _socket.PreferInlineCompletions;
+ }
+
private void Register()
{
Debug.Assert(_nonBlockingSet);
@@ -2051,15 +2065,33 @@ public Interop.Sys.SocketEvents HandleSyncEventsSpeculatively(Interop.Sys.Socket
return events;
}
- public unsafe void HandleEvents(Interop.Sys.SocketEvents events)
+ // Called on the epoll thread.
+ public void HandleEventsInline(Interop.Sys.SocketEvents events)
{
if ((events & Interop.Sys.SocketEvents.Error) != 0)
{
- // Set the Read and Write flags as well; the processing for these events
+ // Set the Read and Write flags; the processing for these events
// will pick up the error.
+ events ^= Interop.Sys.SocketEvents.Error;
events |= Interop.Sys.SocketEvents.Read | Interop.Sys.SocketEvents.Write;
}
+ if ((events & Interop.Sys.SocketEvents.Read) != 0)
+ {
+ _receiveQueue.ProcessSyncEventOrGetAsyncEvent(this, processAsyncEvents: true);
+ }
+
+ if ((events & Interop.Sys.SocketEvents.Write) != 0)
+ {
+ _sendQueue.ProcessSyncEventOrGetAsyncEvent(this, processAsyncEvents: true);
+ }
+ }
+
+ // Called on ThreadPool thread.
+ public unsafe void HandleEvents(Interop.Sys.SocketEvents events)
+ {
+ Debug.Assert((events & Interop.Sys.SocketEvents.Error) == 0);
+
AsyncOperation? receiveOperation =
(events & Interop.Sys.SocketEvents.Read) != 0 ? _receiveQueue.ProcessSyncEventOrGetAsyncEvent(this) : null;
AsyncOperation? sendOperation =
diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Unix.cs
index 18605879cb12db..99380b31cb0270 100644
--- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Unix.cs
+++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Unix.cs
@@ -19,6 +19,12 @@ internal sealed unsafe class SocketAsyncEngine : IThreadPoolWorkItem
1024;
#endif
+ // Socket continuations are dispatched to the ThreadPool from the event thread.
+ // This avoids continuations blocking the event handling.
+ // Setting PreferInlineCompletions allows continuations to run directly on the event thread.
+ // PreferInlineCompletions defaults to false and can be set to true using the DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS envvar.
+ internal static readonly bool InlineSocketCompletionsEnabled = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1";
+
private static int GetEngineCount()
{
// The responsibility of SocketAsyncEngine is to get notifications from epoll|kqueue
@@ -38,6 +44,12 @@ private static int GetEngineCount()
return (int)count;
}
+ // When inlining continuations, we default to ProcessorCount to make sure event threads cannot be a bottleneck.
+ if (InlineSocketCompletionsEnabled)
+ {
+ return Environment.ProcessorCount;
+ }
+
Architecture architecture = RuntimeInformation.ProcessArchitecture;
int coresPerEngine = architecture == Architecture.Arm64 || architecture == Architecture.Arm
? 8
@@ -195,17 +207,25 @@ private void EventLoop()
if (handleToContextMap.TryGetValue(handle, out SocketAsyncContextWrapper contextWrapper) && (context = contextWrapper.Context) != null)
{
- Interop.Sys.SocketEvents events = context.HandleSyncEventsSpeculatively(socketEvent.Events);
- if (events != Interop.Sys.SocketEvents.None)
+ if (context.PreferInlineCompletions)
+ {
+ context.HandleEventsInline(socketEvent.Events);
+ }
+ else
{
- var ev = new SocketIOEvent(context, events);
- eventQueue.Enqueue(ev);
- enqueuedEvent = true;
-
- // This is necessary when the JIT generates unoptimized code (debug builds, live debugging,
- // quick JIT, etc.) to ensure that the context does not remain referenced by this method, as
- // such code may keep the stack location live for longer than necessary
- ev = default;
+ Interop.Sys.SocketEvents events = context.HandleSyncEventsSpeculatively(socketEvent.Events);
+
+ if (events != Interop.Sys.SocketEvents.None)
+ {
+ var ev = new SocketIOEvent(context, events);
+ eventQueue.Enqueue(ev);
+ enqueuedEvent = true;
+
+ // This is necessary when the JIT generates unoptimized code (debug builds, live debugging,
+ // quick JIT, etc.) to ensure that the context does not remain referenced by this method, as
+ // such code may keep the stack location live for longer than necessary
+ ev = default;
+ }
}
// This is necessary when the JIT generates unoptimized code (debug builds, live debugging,
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/InlineCompletions.Unix.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/InlineCompletions.Unix.cs
new file mode 100644
index 00000000000000..a49dc45a6f1fd4
--- /dev/null
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/InlineCompletions.Unix.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using Microsoft.DotNet.RemoteExecutor;
+
+namespace System.Net.Sockets.Tests
+{
+ public class InlineContinuations
+ {
+ [OuterLoop]
+ [Fact]
+ [PlatformSpecific(TestPlatforms.AnyUnix)] // Inline Socket mode is specific to Unix Socket implementation.
+ public void InlineSocketContinuations()
+ {
+ RemoteInvokeOptions options = new RemoteInvokeOptions();
+ options.StartInfo.EnvironmentVariables.Add("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS", "1");
+ options.TimeOut = (int)TimeSpan.FromMinutes(20).TotalMilliseconds;
+
+ RemoteExecutor.Invoke(async () =>
+ {
+ // Connect/Accept tests
+ await new AcceptEap(null).Accept_ConcurrentAcceptsBeforeConnects_Success(5);
+ await new AcceptEap(null).Accept_ConcurrentAcceptsAfterConnects_Success(5);
+
+ // Send/Receive tests
+ await new SendReceiveEap(null).SendRecv_Stream_TCP(IPAddress.Loopback, useMultipleBuffers: false);
+ await new SendReceiveEap(null).SendRecv_Stream_TCP_MultipleConcurrentReceives(IPAddress.Loopback, useMultipleBuffers: false);
+ await new SendReceiveEap(null).SendRecv_Stream_TCP_MultipleConcurrentSends(IPAddress.Loopback, useMultipleBuffers: false);
+ await new SendReceiveEap(null).TcpReceiveSendGetsCanceledByDispose(receiveOrSend: true);
+ await new SendReceiveEap(null).TcpReceiveSendGetsCanceledByDispose(receiveOrSend: false);
+ }, options).Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendReceive.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendReceive.cs
index 13185b66b4f5b3..bab3e35d970288 100644
--- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendReceive.cs
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendReceive.cs
@@ -1764,6 +1764,7 @@ public async Task DisposedSocket_ThrowsOperationCanceledException()
public async Task BlockingAsyncContinuations_OperationsStillCompleteSuccessfully()
{
if (UsesSync) return;
+ if (Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1") return;
using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/System.Net.Sockets.Tests.csproj b/src/libraries/System.Net.Sockets/tests/FunctionalTests/System.Net.Sockets.Tests.csproj
index ab01170df35f44..c684dc6463b797 100644
--- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/System.Net.Sockets.Tests.csproj
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/System.Net.Sockets.Tests.csproj
@@ -18,6 +18,7 @@
+