Skip to content

[API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

Description

@kouvel

Background and motivation

Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

More discussion here: #47631

API Proposal

Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

RegisterForIOReadyNotifications

  • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
  • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
  • Arguments
    • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
    • events - A mask of events for which to register for notifications
    • eventHandler - An object that would handle IO-ready events for the IO handle
  • Exceptions
    • ArgumentNullException - One of the arguments is null
    • ArgumentException - handle is closed or invalid
    • ArgumentOutOfRangeException - The value of events is invalid
    • InvalidOperationException - handle was already registered
    • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

UnregisterForIOReadyNotifications

  • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
  • Arguments
    • handle - The IO handle to unregister for new IO-ready event notifications
  • Exceptions
    • ArgumentNullException - One of the arguments is null
    • InvalidOperationException - handle was not registered

IOReadyEvents

  • Read / Write - The stream is ready for a read or write respectively
  • CloseOrError
    • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
    • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
    • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

IIOReadyEventHandler

  • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
    • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
    • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
    • This method is called immediately upon receiving an event for inline processing
    • events specifies which IO-ready events were triggered
    • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
  • voidHandleEvents(IOReadyEventsevents);
    • This method is called for handling events that were returned by the method above

API Usage

These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

Alternative Designs

  • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
  • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
  • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
  • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

Risks

  • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
    Skip to content

    [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

    Description

    @kouvel

    Background and motivation

    Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

    The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

    More discussion here: #47631

    API Proposal

    Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

    namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

    RegisterForIOReadyNotifications

    • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
    • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
    • Arguments
      • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
      • events - A mask of events for which to register for notifications
      • eventHandler - An object that would handle IO-ready events for the IO handle
    • Exceptions
      • ArgumentNullException - One of the arguments is null
      • ArgumentException - handle is closed or invalid
      • ArgumentOutOfRangeException - The value of events is invalid
      • InvalidOperationException - handle was already registered
      • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

    UnregisterForIOReadyNotifications

    • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
    • Arguments
      • handle - The IO handle to unregister for new IO-ready event notifications
    • Exceptions
      • ArgumentNullException - One of the arguments is null
      • InvalidOperationException - handle was not registered

    IOReadyEvents

    • Read / Write - The stream is ready for a read or write respectively
    • CloseOrError
      • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
      • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
      • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

    IIOReadyEventHandler

    • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
      • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
      • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
      • This method is called immediately upon receiving an event for inline processing
      • events specifies which IO-ready events were triggered
      • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
    • voidHandleEvents(IOReadyEventsevents);
      • This method is called for handling events that were returned by the method above

    API Usage

    These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

    The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

    internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
    fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
    fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

    Alternative Designs

    • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
    • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
    • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
    • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

    Risks

    • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

    Metadata

    Metadata

    Assignees

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
      Skip to content

      [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

      Description

      @kouvel

      Background and motivation

      Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

      The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

      More discussion here: #47631

      API Proposal

      Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

      namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

      RegisterForIOReadyNotifications

      • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
      • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
      • Arguments
        • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
        • events - A mask of events for which to register for notifications
        • eventHandler - An object that would handle IO-ready events for the IO handle
      • Exceptions
        • ArgumentNullException - One of the arguments is null
        • ArgumentException - handle is closed or invalid
        • ArgumentOutOfRangeException - The value of events is invalid
        • InvalidOperationException - handle was already registered
        • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

      UnregisterForIOReadyNotifications

      • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
      • Arguments
        • handle - The IO handle to unregister for new IO-ready event notifications
      • Exceptions
        • ArgumentNullException - One of the arguments is null
        • InvalidOperationException - handle was not registered

      IOReadyEvents

      • Read / Write - The stream is ready for a read or write respectively
      • CloseOrError
        • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
        • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
        • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

      IIOReadyEventHandler

      • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
        • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
        • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
        • This method is called immediately upon receiving an event for inline processing
        • events specifies which IO-ready events were triggered
        • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
      • voidHandleEvents(IOReadyEventsevents);
        • This method is called for handling events that were returned by the method above

      API Usage

      These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

      The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

      internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
      fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
      fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

      Alternative Designs

      • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
      • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
      • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
      • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

      Risks

      • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

      Metadata

      Metadata

      Assignees

      Labels

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
        Skip to content

        [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

        Description

        @kouvel

        Background and motivation

        Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

        The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

        More discussion here: #47631

        API Proposal

        Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

        namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

        RegisterForIOReadyNotifications

        • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
        • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
        • Arguments
          • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
          • events - A mask of events for which to register for notifications
          • eventHandler - An object that would handle IO-ready events for the IO handle
        • Exceptions
          • ArgumentNullException - One of the arguments is null
          • ArgumentException - handle is closed or invalid
          • ArgumentOutOfRangeException - The value of events is invalid
          • InvalidOperationException - handle was already registered
          • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

        UnregisterForIOReadyNotifications

        • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
        • Arguments
          • handle - The IO handle to unregister for new IO-ready event notifications
        • Exceptions
          • ArgumentNullException - One of the arguments is null
          • InvalidOperationException - handle was not registered

        IOReadyEvents

        • Read / Write - The stream is ready for a read or write respectively
        • CloseOrError
          • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
          • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
          • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

        IIOReadyEventHandler

        • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
          • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
          • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
          • This method is called immediately upon receiving an event for inline processing
          • events specifies which IO-ready events were triggered
          • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
        • voidHandleEvents(IOReadyEventsevents);
          • This method is called for handling events that were returned by the method above

        API Usage

        These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

        The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

        internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
        fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
        fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

        Alternative Designs

        • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
        • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
        • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
        • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

        Risks

        • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

        Metadata

        Metadata

        Assignees

        Labels

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
          Skip to content

          [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

          Description

          @kouvel

          Background and motivation

          Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

          The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

          More discussion here: #47631

          API Proposal

          Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

          namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

          RegisterForIOReadyNotifications

          • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
          • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
          • Arguments
            • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
            • events - A mask of events for which to register for notifications
            • eventHandler - An object that would handle IO-ready events for the IO handle
          • Exceptions
            • ArgumentNullException - One of the arguments is null
            • ArgumentException - handle is closed or invalid
            • ArgumentOutOfRangeException - The value of events is invalid
            • InvalidOperationException - handle was already registered
            • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

          UnregisterForIOReadyNotifications

          • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
          • Arguments
            • handle - The IO handle to unregister for new IO-ready event notifications
          • Exceptions
            • ArgumentNullException - One of the arguments is null
            • InvalidOperationException - handle was not registered

          IOReadyEvents

          • Read / Write - The stream is ready for a read or write respectively
          • CloseOrError
            • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
            • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
            • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

          IIOReadyEventHandler

          • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
            • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
            • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
            • This method is called immediately upon receiving an event for inline processing
            • events specifies which IO-ready events were triggered
            • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
          • voidHandleEvents(IOReadyEventsevents);
            • This method is called for handling events that were returned by the method above

          API Usage

          These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

          The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

          internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
          fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
          fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

          Alternative Designs

          • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
          • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
          • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
          • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

          Risks

          • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

          Metadata

          Metadata

          Assignees

          Labels

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
            Skip to content

            [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

            Description

            @kouvel

            Background and motivation

            Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

            The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

            More discussion here: #47631

            API Proposal

            Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

            namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

            RegisterForIOReadyNotifications

            • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
            • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
            • Arguments
              • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
              • events - A mask of events for which to register for notifications
              • eventHandler - An object that would handle IO-ready events for the IO handle
            • Exceptions
              • ArgumentNullException - One of the arguments is null
              • ArgumentException - handle is closed or invalid
              • ArgumentOutOfRangeException - The value of events is invalid
              • InvalidOperationException - handle was already registered
              • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

            UnregisterForIOReadyNotifications

            • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
            • Arguments
              • handle - The IO handle to unregister for new IO-ready event notifications
            • Exceptions
              • ArgumentNullException - One of the arguments is null
              • InvalidOperationException - handle was not registered

            IOReadyEvents

            • Read / Write - The stream is ready for a read or write respectively
            • CloseOrError
              • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
              • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
              • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

            IIOReadyEventHandler

            • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
              • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
              • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
              • This method is called immediately upon receiving an event for inline processing
              • events specifies which IO-ready events were triggered
              • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
            • voidHandleEvents(IOReadyEventsevents);
              • This method is called for handling events that were returned by the method above

            API Usage

            These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

            The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

            internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
            fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
            fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

            Alternative Designs

            • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
            • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
            • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
            • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

            Risks

            • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

            Metadata

            Metadata

            Assignees

            Labels

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
              Skip to content

              [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

              Description

              @kouvel

              Background and motivation

              Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

              The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

              More discussion here: #47631

              API Proposal

              Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

              namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

              RegisterForIOReadyNotifications

              • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
              • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
              • Arguments
                • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
                • events - A mask of events for which to register for notifications
                • eventHandler - An object that would handle IO-ready events for the IO handle
              • Exceptions
                • ArgumentNullException - One of the arguments is null
                • ArgumentException - handle is closed or invalid
                • ArgumentOutOfRangeException - The value of events is invalid
                • InvalidOperationException - handle was already registered
                • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

              UnregisterForIOReadyNotifications

              • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
              • Arguments
                • handle - The IO handle to unregister for new IO-ready event notifications
              • Exceptions
                • ArgumentNullException - One of the arguments is null
                • InvalidOperationException - handle was not registered

              IOReadyEvents

              • Read / Write - The stream is ready for a read or write respectively
              • CloseOrError
                • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
                • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
                • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

              IIOReadyEventHandler

              • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
                • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
                • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
                • This method is called immediately upon receiving an event for inline processing
                • events specifies which IO-ready events were triggered
                • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
              • voidHandleEvents(IOReadyEventsevents);
                • This method is called for handling events that were returned by the method above

              API Usage

              These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

              The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

              internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
              fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
              fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

              Alternative Designs

              • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
              • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
              • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
              • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

              Risks

              • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

              Metadata

              Metadata

              Assignees

              Labels

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool · Issue #61273 · dotnet/runtime · GitHub
                Skip to content

                [API Proposal]: Add Unix-specific APIs to register for IO ready notifications on the thread pool #61273

                Description

                @kouvel

                Background and motivation

                Currently we don't have a common way of handling IO readiness and completions across operating systems. On Windows the IO completion thread pool is not very efficient. The mechanism used on Linux in System.Net.Sockets is more efficient, but comes with other issues. As part of adding IO readiness/completion handling to the portable thread pool (transitioning away from the native thread pool), we would also like to consolidate the mechanisms used for IO readiness/completion handling across operating systems. Hopefully this would ease maintenance, make it easier for other components to also use the functionality, and enable further experimentation for improving things across the various async IO mechanisms, such as potentially with better thread management and better ordering of work.

                The APIs being proposed here are specific to epoll/kqueue, analogous to the existing APIs for overlapped IO on Windows. Ideally, we would move to io_uring on Linux, but it's not available in all distros yet and we would continue to need a kqueue-based solution. When we decide to add support for io_uring, it would likely require a separate set of APIs.

                More discussion here: #47631

                API Proposal

                Update: It's not necessary to expose these APIs in contracts, so the current proposal is to make them public for use in .NET libraries but not expose them in contracts for public usage.

                namespaceSystem.Threading{publicstaticclassThreadPool{[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidRegisterForIOReadyNotifications(SafeHandlehandle,IOReadyEventsevents,IIOReadyEventHandlereventHandler){}[UnsupportedOSPlatform("windows")][UnsupportedOSPlatform("browser")]publicstaticvoidUnregisterForIOReadyNotifications(SafeHandlehandle){}}[Flags]publicenumIOReadyEvents{None=0x0,Read=0x1,Write=0x2,CloseOrError=0x4}publicinterfaceIIOReadyEventHandler{IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);voidHandleEvents(IOReadyEventsevents);}}

                RegisterForIOReadyNotifications

                • Registers an IO handle to receive notifications of IO-ready events. The event handler is notified of IO-ready events for the IO handle for inline processing, or asynchronous processing on thread pool worker threads.
                • This API is specific to Unix-like platforms and uses epoll or kqueue. The underlying implementation may not support some types of IO handles.
                • Arguments
                  • handle - The IO handle to register for IO-ready event notifications. The safe handle wraps a file descriptor that typically represents a non-blocking pipe or socket. An IO handle is keyed by the handle value and may be registered only once.
                  • events - A mask of events for which to register for notifications
                  • eventHandler - An object that would handle IO-ready events for the IO handle
                • Exceptions
                  • ArgumentNullException - One of the arguments is null
                  • ArgumentException - handle is closed or invalid
                  • ArgumentOutOfRangeException - The value of events is invalid
                  • InvalidOperationException - handle was already registered
                  • IOException - An error occurred when attempting to register the IO handle for notifications. The HResult property would contain an error code for further investigation

                UnregisterForIOReadyNotifications

                • Unregisters an IO handle to no longer receive notifications of new IO-ready events. Any IO-ready events that were already received prior to unregisteration are still delivered to the previously registered event handler.
                • Arguments
                  • handle - The IO handle to unregister for new IO-ready event notifications
                • Exceptions
                  • ArgumentNullException - One of the arguments is null
                  • InvalidOperationException - handle was not registered

                IOReadyEvents

                • Read / Write - The stream is ready for a read or write respectively
                • CloseOrError
                  • There appear to be some inconsistencies in how epoll reports the RDHUP, HUP, and ERR events on Linux, see Add test to list OS event details tokio-rs/mio#1091 (comment). To simplify things, for epoll when any of those are set, we could just include this bit and have the user determine what happened based on the error code from the following IO operation.
                  • For kqueue, similarly this bit would be set when EV_EOF or EV_ERROR are reported
                  • System.Net.Sockets could continue to do what it's doing now and treat CloseOrError as Read | Write

                IIOReadyEventHandler

                • IOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents);
                  • Currently on Unix, when a synchronous operation is performed on a non-blocking socket, after the operation completes the blocking thread is released immediately upon receiving the event instead of queuing event processing to the thread pool, since otherwise the blocking may have occurred on a thread pool thread and lead to thread starvation
                  • There is currently a config option to force processing events on the polling threads inline (along with configuring the number of polling threads), which we are currently planning to keep intact, at least for now
                  • This method is called immediately upon receiving an event for inline processing
                  • events specifies which IO-ready events were triggered
                  • The returned events, if not None, will be queued for processing and will be sent later through HandleEvents. So, a user may choose to process all or some of the events inline depending on whether there is a pending synchronous operation waiting to unblock from the event.
                • voidHandleEvents(IOReadyEventsevents);
                  • This method is called for handling events that were returned by the method above

                API Usage

                These APIs are basically a minimum form of interface between the current SocketAsyncContext and SocketAsyncEngine. SocketAsyncContext would be the initial consumer: registration, unregistration, handling sync events inline, and handling events normally.

                The APIs are not necessarily easy to use, they're meant to provide a minimal support for epoll/kqueue-based polling with handling events in thread pool threads. Here's some oversimplified pseudocode on how the APIs may be used, to read some data from a socket, process it, and write a response:

                internalsealedclassClientSocketIOHandler:IIOReadyEventHandler{SafeHandle_socketHandle;bool_isReading;byte[]_readBuffer;int_nextReadIndex;byte[]_writeBuffer;int_byteCountToWrite;int_nextWriteStartIndex;publicClientSocketIOHandler(SafeHandlesocketHandle){_socketHandle=socketHandle;_isReading=true;// Change the socket to non-blockingThreadPool.RegisterForIOReadyNotifications(_socketHandle,IOReadyEvents.Read|IOReadyEvents.Write|IOReadyEvents.CloseOrError,this);}publicIOReadyEventsHandleEventsInlineIfNecessary(IOReadyEventsevents)=>events;publicunsafevoidHandleEvents(IOReadyEventsevents){if((events&IOReadyEvents.CloseOrError)!=0){events|=IOReadyEvents.Read|IOReadyEvents.Write;}lock(this){if((events&IOReadyEvents.Read)!=0&&_isReading){if(_readBuffer==null){_readBuffer=newbyte[256];}varspan=newSpan<byte>(_readBuffer,_nextReadIndex,_readBuffer.Length-_nextReadIndex);intbytesReceived,errno;
                fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Recv(_socketHandle,b,span.Length,0,outbytesReceived);}if(bytesReceived<=0)// error{_isReading=false;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextReadIndex+=bytesReceived;if(/* enough data read */)// done reading{_isReading=false;// Process the data, fill _writeBuffer_readBuffer=null;events|=IOReadyEvents.Write;}}if((events&IOReadyEvents.Write)!=0&&_writeBuffer!=null){varspan=newSpan<byte>(_writeBuffer,_nextWriteStartIndex,_byteCountToWrite-_nextWriteStartIndex);intbytesSent,errno;
                fixed (byte*b=&MemoryMarshal.GetReference(span)){errno=Interop.Send(_socketHandle,b,span.Length,0,outbytesSent);}if(bytesSent<=0)// error{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);return;}_nextWriteStartIndex+=bytesSent;if(_nextWriteStartIndex>=_byteCountToWrite)// done writing{_writeBuffer=null;ThreadPool.UnregisterForIOReadyNotifications(_socketHandle);// Close the socket}}}}}

                Alternative Designs

                • UnregisterForIOReadyNotifications - A separate method is proposed for unregistration rather than returning a disposable from the Register method. The expectation is that the event handler object would manage the lifetime of registration along with its own lifetime. A separate method for unregistration would avoid some unnecessary allocation for each IO handle that is registered.
                • IOReadyEvents.CloseOrError - Alternatively, we could include all of the various bits (currently named ReadClose, Close, and Error in the implementation), translate to those exactly as how the system provides, and let the user disambiguate
                • InternalsVisibleTo or reflection instead of making the APIs public - As far as I could tell, InternalsVisibleTo doesn't seem to be used anymore except for tests. Reflection is possible, though perhaps inconvenient.
                • When thinking about adding APIs for io_uring it may make sense to put those APIs under System.IO (though with implementation in CoreLib), since it may need an API for each op code. Not sure if there would be a better place for these epoll/kqueue-based APIs than directly on the thread pool.

                Risks

                • Registering without unregistering would add to bookkeeping memory. Typically when all file descriptors representing a particular stream are closed, it would not receive events anymore, but there would be some bookkeeping cost from not unregistering. I feel it's a low risk, as it's a fairly low-level API.

                Metadata

                Metadata

                Assignees

                Labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions