diff --git a/NEWS.md b/NEWS.md index 5a43a7a5d3..e6c77e0977 100644 --- a/NEWS.md +++ b/NEWS.md @@ -161,11 +161,51 @@ Outstanding ones only. p C.ancestors #=> [C, M1, M2, Object, Kernel, BasicObject] ``` +* Thread + + * Introduce `Thread#scheduler` for intercepting blocking operations and + `Thread.scheduler` for accessing the current scheduler. See + doc/scheduler.md for more details. [[Feature #16786]] + * `Thread#blocking?` tells whether the current execution context is + blocking. [[Feature #16786]] + * `Thread#join` invokes the scheduler hooks `block`/`unblock` in a + non-blocking execution context. [[Feature #16786]] + * Mutex - * Mutex is now acquired per-Fiber instead of per-Thread. This change should - be compatible for essentially all usages and avoids blocking when using - a Fiber Scheduler. [[Feature #16792]] + * `Mutex` is now acquired per-`Fiber` instead of per-`Thread`. This change + should be compatible for essentially all usages and avoids blocking when + using a scheduler. [[Feature #16792]] + +* Fiber + + * `Fiber.new(blocking: true/false)` allows you to create non-blocking + execution contexts. [[Feature #16786]] + * `Fiber#blocking?` tells whether the fiber is non-blocking. [[Feature #16786]] + +* Kernel + + * `Kernel.sleep(...)` invokes the scheduler hook `#kernel_sleep(...)` in a + non-blocking execution context. [[Feature #16786]] + +* IO + + * `IO#nonblock?` now defaults to `true`. [[Feature #16786]] + * `IO#wait_readable`, `IO#wait_writable`, `IO#read`, `IO#write` and other + related methods (e.g. `#puts`, `#gets`) may invoke the scheduler hook + `#io_wait(io, events, timeout)` in a non-blocking execution context. + [[Feature #16786]] + +* ConditionVariable + + * `ConditionVariable#wait` may now invoke the `block`/`unblock` scheduler + hooks in a non-blocking context. [[Feature #16786]] + +* Queue / SizedQueue + + * `Queue#pop`, `SizedQueue#push` and related methods may now invoke the + `block`/`unblock` scheduler hooks in a non-blocking context. + [[Feature #16786]] * Ractor @@ -381,6 +421,7 @@ Excluding feature bug fixes. [Feature #16686]: https://bugs.ruby-lang.org/issues/16686 [Feature #16746]: https://bugs.ruby-lang.org/issues/16746 [Feature #16754]: https://bugs.ruby-lang.org/issues/16754 +[Feature #16786]: https://bugs.ruby-lang.org/issues/16786 [Feature #16792]: https://bugs.ruby-lang.org/issues/16792 [Feature #16828]: https://bugs.ruby-lang.org/issues/16828 [Misc #16961]: https://bugs.ruby-lang.org/issues/16961 diff --git a/doc/fiber.rdoc b/doc/fiber.rdoc deleted file mode 100644 index 584e67ffca..0000000000 --- a/doc/fiber.rdoc +++ /dev/null @@ -1,137 +0,0 @@ -= Fiber - -Fiber is a flow-control primitive which enable cooperative scheduling. This is -in contrast to threads which can be preemptively scheduled at any time. While -having a similar memory profiles, the cost of context switching fibers can be -significantly less than threads as it does not involve a system call. - -== Design - -=== Scheduler - -The per-thread fiber scheduler interface is used to intercept blocking -operations. A typical implementation would be a wrapper for a gem like -EventMachine or Async. This design provides separation of concerns between the -event loop implementation and application code. It also allows for layered -schedulers which can perform instrumentation. - - class Scheduler - # Wait for the given file descriptor to become readable. - def wait_readable(io) - end - - # Wait for the given file descriptor to become writable. - def wait_writable(io) - end - - # Wait for the given file descriptor to match the specified events within - # the specified timeout. - # @param event [Integer] a bit mask of +IO::WAIT_READABLE+, - # `IO::WAIT_WRITABLE` and `IO::WAIT_PRIORITY`. - # @param timeout [#to_f] the amount of time to wait for the event. - def wait_any(io, events, timeout) - end - - # Sleep the current task for the specified duration, or forever if not - # specified. - # @param duration [#to_f] the amount of time to sleep. - def wait_sleep(duration = nil) - end - - # The Ruby virtual machine is going to enter a system level blocking - # operation. - def enter_blocking_region - end - - # The Ruby virtual machine has completed the system level blocking - # operation. - def exit_blocking_region - end - - # Intercept the creation of a non-blocking fiber. - def fiber(&block) - Fiber.new(blocking: false, &block) - end - - # Invoked when the thread exits. - def run - # Implement event loop here. - end - end - -On CRuby, the following extra methods need to be implemented to handle the -public C interface: - - class Scheduler - # Wrapper for rb_wait_readable(int) C function. - def wait_readable_fd(fd) - wait_readable(::IO.for_fd(fd, autoclose: false)) - end - - # Wrapper for rb_wait_readable(int) C function. - def wait_writable_fd(fd) - wait_writable(::IO.for_fd(fd, autoclose: false)) - end - - # Wrapper for rb_wait_for_single_fd(int) C function. - def wait_for_single_fd(fd, events, duration) - wait_any(::IO.for_fd(fd, autoclose: false), events, duration) - end - end - -=== Non-blocking Fibers - -By default fibers are blocking. Non-blocking fibers may invoke specific -scheduler hooks when a blocking operation occurs, and these hooks may introduce -context switching points. - - Fiber.new(blocking: false) do - puts Fiber.current.blocking? # false - - # May invoke `Thread.current.scheduler&.wait_readable`. - io.read(...) - - # May invoke `Thread.current.scheduler&.wait_writable`. - io.write(...) - - # Will invoke `Thread.current.scheduler&.wait_sleep`. - sleep(n) - end.resume - -We also introduce a new method which simplifies the creation of these -non-blocking fibers: - - Fiber.schedule do - puts Fiber.current.blocking? # false - end - -The purpose of this method is to allow the scheduler to internally decide the -policy for when to start the fiber, and whether to use symmetric or asymmetric -fibers. - -=== Mutex - -Locking a mutex causes the +Thread#scheduler+ to not be used while the mutex -is held by that thread. On +Mutex#lock+, fiber switching via the scheduler -is disabled and operations become blocking for all fibers of the same +Thread+. -On +Mutex#unlock+, the scheduler is enabled again. - - mutex = Mutex.new - - puts Thread.current.blocking? # 1 (true) - - Fiber.new(blocking: false) do - puts Thread.current.blocking? # false - mutex.synchronize do - puts Thread.current.blocking? # (1) true - end - - puts Thread.current.blocking? # false - end.resume - -=== Non-blocking I/O - -By default, I/O is non-blocking. Not all operating systems support non-blocking -I/O. Windows is a notable example where socket I/O can be non-blocking but pipe -I/O is blocking. Provided that there *is* a scheduler and the current thread *is -non-blocking*, the operation will invoke the scheduler. diff --git a/doc/scheduler.md b/doc/scheduler.md new file mode 100644 index 0000000000..e641dabcba --- /dev/null +++ b/doc/scheduler.md @@ -0,0 +1,127 @@ +# Scheduler + +The scheduler interface is used to intercept blocking operations. A typical +implementation would be a wrapper for a gem like `EventMachine` or `Async`. This +design provides separation of concerns between the event loop implementation +and application code. It also allows for layered schedulers which can perform +instrumentation. + +## Interface + +This is the interface you need to implement. + +~~~ ruby +class Scheduler + # Wait for the given file descriptor to match the specified events within + # the specified timeout. + # @parameter event [Integer] A bit mask of `IO::READABLE`, + # `IO::WRITABLE` and `IO::PRIORITY`. + # @parameter timeout [Numeric] The amount of time to wait for the event in seconds. + # @returns [Integer] The subset of events that are ready. + def io_wait(io, events, timeout) + end + + # Sleep the current task for the specified duration, or forever if not + # specified. + # @param duration [Numeric] The amount of time to sleep in seconds. + def kernel_sleep(duration = nil) + end + + # Block the calling fiber. + # @parameter blocker [Object] What we are waiting on, informational only. + # @parameter timeout [Numeric | Nil] The amount of time to wait for in seconds. + # @returns [Boolean] Whether the blocking operation was successful or not. + def block(blocker, timeout = nil) + end + + # Unblock the specified fiber. + # @parameter blocker [Object] What we are waiting on, informational only. + # @parameter fiber [Fiber] The fiber to unblock. + # @reentrant Thread safe. + def unblock(blocker, fiber) + end + + # Intercept the creation of a non-blocking fiber. + # @returns [Fiber] + def fiber(&block) + Fiber.new(blocking: false, &block) + end + + # Invoked when the thread exits. + def close + self.run + end + + def run + # Implement event loop here. + end +end +~~~ + +Additional hooks may be introduced in the future, we will use feature detection +in order to enable these hooks. + +## Non-blocking Execution + +The scheduler hooks will only be used in special non-blocking execution +contexts. Non-blocking execution contexts introduce non-determinism because the +execution of scheduler hooks may introduce context switching points into your +program. + +### Fibers + +Fibers can be used to create non-blocking execution contexts. + +~~~ ruby +Fiber.new(blocking: false) do + puts Fiber.current.blocking? # false + + # May invoke `Thread.scheduler&.io_wait`. + io.read(...) + + # May invoke `Thread.scheduler&.io_wait`. + io.write(...) + + # Will invoke `Thread.scheduler&.kernel_sleep`. + sleep(n) +end.resume +~~~ + +We also introduce a new method which simplifies the creation of these +non-blocking fibers: + +~~~ ruby +Fiber.schedule do + puts Fiber.current.blocking? # false +end +~~~ + +The purpose of this method is to allow the scheduler to internally decide the +policy for when to start the fiber, and whether to use symmetric or asymmetric +fibers. + +### IO + +By default, I/O is non-blocking. Not all operating systems support non-blocking +I/O. Windows is a notable example where socket I/O can be non-blocking but pipe +I/O is blocking. Provided that there *is* a scheduler and the current thread *is +non-blocking*, the operation will invoke the scheduler. + +### Mutex + +The `Mutex` class can be used in a non-blocking context and is fiber specific. + +### ConditionVariable + +The `ConditionVariable` class can be used in a non-blocking context and is +fiber-specific. + +### Queue / SizedQueue + +The `Queue` and `SizedQueue` classses can be used in a non-blocking context and +are fiber-specific. + +### Thread + +The `Thread#join` operation can be used in a non-blocking context and is +fiber-specific.