1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Update NEWS & documentation relating to scheduler.

This commit is contained in:
Samuel Williams 2020-09-21 13:36:34 +12:00
parent 70f08f1eed
commit f7aa51b2b8
Notes: git 2020-09-21 12:28:34 +09:00
3 changed files with 171 additions and 140 deletions

47
NEWS.md
View file

@ -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

View file

@ -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.

127
doc/scheduler.md Normal file
View file

@ -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.