diff --git a/src/libraries/System.Net.Sockets/src/Resources/Strings.resx b/src/libraries/System.Net.Sockets/src/Resources/Strings.resx index 7a0feca077c032..36faf4552f604c 100644 --- a/src/libraries/System.Net.Sockets/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Sockets/src/Resources/Strings.resx @@ -312,9 +312,6 @@ System.Net.Sockets is not supported on this platform. - - Handle is already used by another Socket. - Provided SocketAddress is too small for given AddressFamily. 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 8463c5142b573c..4cc7a28fca8143 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 @@ -1262,6 +1262,8 @@ public void Trace(SocketAsyncContext context, string message, [CallerMemberName] private SocketAsyncEngine? _asyncEngine; private bool IsRegistered => _asyncEngine != null; private bool _isHandleNonBlocking = OperatingSystem.IsWasi(); // WASI sockets are always non-blocking, because we don't have another thread which could be blocked + /// An index into 's table of all contexts that are currently . + internal int GlobalContextIndex = -1; private readonly object _registerLock = new object(); @@ -1330,7 +1332,10 @@ public bool StopAndAbort() // We don't need to synchronize with Register. // This method is called when the handle gets released. // The Register method will throw ODE when it tries to use the handle at this point. - _asyncEngine?.UnregisterSocket(_socket.DangerousGetHandle(), this); + if (IsRegistered) + { + SocketAsyncEngine.UnregisterSocket(this); + } return aborted; } 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 7405e579042232..b67af69163bc5e 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -74,14 +75,17 @@ private static SocketAsyncEngine[] CreateEngines() return engines; } + /// + /// Each is assigned an index into this table while registered with a . + /// The index is used as the to quickly map events to s. + /// It is also stored in so that we can efficiently remove it when unregistering the socket. + /// + private static SocketAsyncContext?[] s_registeredContexts = []; + private static readonly Queue s_registeredContextsFreeList = []; + private readonly IntPtr _port; private readonly Interop.Sys.SocketEvent* _buffer; - // - // Maps handle values to SocketAsyncContext instances. - // - private readonly ConcurrentDictionary _handleToContextMap = new ConcurrentDictionary(); - // // Queue of events generated by EventLoop() that would be processed by the thread pool // @@ -119,28 +123,54 @@ public static bool TryRegisterSocket(IntPtr socketHandle, SocketAsyncContext con private bool TryRegisterCore(IntPtr socketHandle, SocketAsyncContext context, out Interop.Error error) { - bool added = _handleToContextMap.TryAdd(socketHandle, new SocketAsyncContextWrapper(context)); - if (!added) + Debug.Assert(context.GlobalContextIndex == -1); + + lock (s_registeredContextsFreeList) { - // Using public SafeSocketHandle(IntPtr) a user can add the same handle - // from a different Socket instance. - throw new InvalidOperationException(SR.net_sockets_handle_already_used); + if (!s_registeredContextsFreeList.TryDequeue(out int index)) + { + int previousLength = s_registeredContexts.Length; + int newLength = Math.Max(4, 2 * previousLength); + + Array.Resize(ref s_registeredContexts, newLength); + + for (int i = previousLength + 1; i < newLength; i++) + { + s_registeredContextsFreeList.Enqueue(i); + } + + index = previousLength; + } + + Debug.Assert(s_registeredContexts[index] is null); + + s_registeredContexts[index] = context; + context.GlobalContextIndex = index; } error = Interop.Sys.TryChangeSocketEventRegistration(_port, socketHandle, Interop.Sys.SocketEvents.None, - Interop.Sys.SocketEvents.Read | Interop.Sys.SocketEvents.Write, socketHandle); + Interop.Sys.SocketEvents.Read | Interop.Sys.SocketEvents.Write, context.GlobalContextIndex); if (error == Interop.Error.SUCCESS) { return true; } - _handleToContextMap.TryRemove(socketHandle, out _); + UnregisterSocket(context); return false; } - public void UnregisterSocket(IntPtr socketHandle, SocketAsyncContext __) + public static void UnregisterSocket(SocketAsyncContext context) { - _handleToContextMap.TryRemove(socketHandle, out _); + Debug.Assert(context.GlobalContextIndex >= 0); + Debug.Assert(ReferenceEquals(s_registeredContexts[context.GlobalContextIndex], context)); + + lock (s_registeredContextsFreeList) + { + s_registeredContexts[context.GlobalContextIndex] = null; + s_registeredContextsFreeList.Enqueue(context.GlobalContextIndex); + } + + context.GlobalContextIndex = -1; } private SocketAsyncEngine() @@ -324,13 +354,11 @@ private readonly struct SocketEventHandler { public Interop.Sys.SocketEvent* Buffer { get; } - private readonly ConcurrentDictionary _handleToContextMap; private readonly ConcurrentQueue _eventQueue; public SocketEventHandler(SocketAsyncEngine engine) { Buffer = engine._buffer; - _handleToContextMap = engine._handleToContextMap; _eventQueue = engine._eventQueue; } @@ -340,10 +368,15 @@ public bool HandleSocketEvents(int numEvents) bool enqueuedEvent = false; foreach (var socketEvent in new ReadOnlySpan(Buffer, numEvents)) { - if (_handleToContextMap.TryGetValue(socketEvent.Data, out SocketAsyncContextWrapper contextWrapper)) - { - SocketAsyncContext context = contextWrapper.Context; + Debug.Assert((uint)socketEvent.Data < (uint)s_registeredContexts.Length); + // The context may be null if the socket was unregistered right before the event was processed. + // The slot in s_registeredContexts may have been reused by a different context, in which case the + // incorrect socket will notice that no information is available yet and harmlessly retry, waiting for new events. + SocketAsyncContext? context = s_registeredContexts[(uint)socketEvent.Data]; + + if (context is not null) + { if (context.PreferInlineCompletions) { context.HandleEventsInline(socketEvent.Events); @@ -365,18 +398,6 @@ public bool HandleSocketEvents(int numEvents) } } - // struct wrapper is used in order to improve the performance of the epoll thread hot path by up to 3% of some TechEmpower benchmarks - // the goal is to have a dedicated generic instantiation and using: - // System.Collections.Concurrent.ConcurrentDictionary`2[System.IntPtr,System.Net.Sockets.SocketAsyncContextWrapper]::TryGetValueInternal(!0,int32,!1&) - // instead of: - // System.Collections.Concurrent.ConcurrentDictionary`2[System.IntPtr,System.__Canon]::TryGetValueInternal(!0,int32,!1&) - private readonly struct SocketAsyncContextWrapper - { - public SocketAsyncContextWrapper(SocketAsyncContext context) => Context = context; - - internal SocketAsyncContext Context { get; } - } - private readonly struct SocketIOEvent { public SocketAsyncContext Context { get; } diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Wasi.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Wasi.cs index 3b69902260d812..0a39feb2699364 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Wasi.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEngine.Wasi.cs @@ -35,9 +35,7 @@ public static bool TryRegisterSocket(IntPtr socketHandle, SocketAsyncContext con return true; } -#pragma warning disable CA1822 - public void UnregisterSocket(IntPtr _, SocketAsyncContext context) -#pragma warning restore CA1822 + public static void UnregisterSocket(SocketAsyncContext context) { context.unregisterPollHook.Cancel(); }