-
Notifications
You must be signed in to change notification settings - Fork 1
Multi Threading
Ruby offers a number of options for multi-threaded code - Threads, Fibers and Continuations. Note however, there is no support for parallelism - MRI Ruby will only do one thing at a time.
Ruby's Threads are its richest form of multithreading, and work as follows:
- They implement cooperative multi-threading - only one thread runs at a time, and it runs until it voluntarily yields its control and another thread begins to run.
- Thread yielding is controlled primarily by the Ruby VM, not by the application. Threads will yield according to a set of rules which mimic preemptive multitasking (e.g. after their slice of time has expired).
- They can invoke blocking calls, and other threads may run while a thread is blocked
In the underlying implementation, these properties are achieved because each Ruby thread is a native OS thread, but they all compete to acquire a global mutex-like object called the Global VM Lock (GVL) before they do any work (so only one Thread runs at a time), and then release the lock under certain circumstances to voluntarily yield control to another Thread.
A thread has a program which it begins executing when the thread is created, and when the program is completed the thread exits. A Ruby program always has at least 1 thread, which is the main thread, and its program is the main sourcefile. Other threads are created by application code with a designated block or Proc as their program.
For non-main threads, their block or Proc runs in a slightly altered execution context. For the purposes of the new thread it is allocated a new special variables region in its local variable context, but the closure on the other local variables is preserved.
When a thread other than the main thread completes, its completion status is made available to other application code:
- If the thread completes normally, the value of its program's statement block is available (through
Thread.value) - If the thread completes by raising an exception out of its program, any other thread that joins on it will have the exception raised out of the join method
- If the thread completes by raising an exception out of its program, for some types of exceptions the exception will also be thrown on the main thread (to be raised at the next interrupt point):
- If the exception is a
SystemExitexception - If the thread's or vm's abort on exception parameter is set
- If the exception is a fatal exception
- Control transfer instructions??
When the main thread completes (either normally or by throwing an exception), Ruby will begin exit processing, even if other threads are still running.
The main thread has another special property - signal handlers are always invoked on the main thread.
At frequent points in the execution of a Ruby thread, there will be an interrupt point in which the Thread can perform some out-of-band actions:
- It will yield control to another thread, if its timeslice has expired
- If the thread has been instructed to raise an exception (e.g. by another thread), it will do so
- If the thread has been killed, it will begin its kill processing
- If the thread is the main thread, it will run any signal handlers that are pending
- If the thread has been instructed to run finalizers, it will do so
In general, you probably shouldn't rely on knowledge of when an interrupt point will occur. Nevertheless, interrupt points occur at the following points (these are not exhaustive, but they are the most typical interrupt points):
- After method calls - although the Ruby VM will by default optimize away some fundamental method calls unless they are overridden (such as arithmetic operators on numeric values)
- When jumping to a different point of Ruby code as a result of a conditional, loop or control transfer expression
- During blocking, if there is work to be done in the interrupt point (e.g. raise exception, kill, or run signal handlers)
This slightly coarse-grained approach can mean that multi-threaded Ruby code is resilient against some typical race condition scenarios. For example, it appears that a series of simple assignments will be executed atomically as there can be no interrupt point between them.
The only mechanism in MRI Ruby whereby one thread can pause running and another thread resume running, is when the current thread yields control. Ruby guarantees that if at least one other thread is available to execute when the current thread yields control, then one of the other available threads will resume, however it is undefined which of the other available threads will resume executing.
Threads will yield control to another thread:
- At interrupt points, if the thread's timeslice has expired. The timeslice is calculated based on the thread's priority setting.
- If instructed to do so by application code (e.g. by the
Thread.passmethod) - If it is entering a blocking operation, including:
- Locking on a Ruby Mutex object
- Sleeping
- Joining on another thread (i.e. waiting for it to complete execution)
- File descriptor I/O operations such as file read/write, network read/write, joining on a process
- Other blocking operations that may be implemented in the Ruby core or extension libraries
When a thread enters a blocking operation, it will do so atomically with releasing control to another thread. This means, in particular, that e.g. if the next thread to be scheduled sees that the thread is blocked and decides to wake it up, the thread is guaranteed to see the wakeup instruction.
A thread can influence the behaviour of other threads by:
- Raising an exception on another thread - the target thread will raise the exception at its next interrupt point
- Killing another thread - the target thread will begin kill processing at its next interrupt point
- Waking another thread from a block
A thread can be woken up from a blocking operation by:
- Raising an exception on the thread or killing the thread
- Issuing a wakeup instruction to the thread (e.g. Thread.wakeup) - note that only certain types of blocking operation are eligible for wakeup, specifically sleep blocking. Thread join, File descriptor I/O and Mutex lock blocking will not be woken up by this instruction
- When the main thread is in a blocking operation, it will be woken up to run a signal handler if one is pending, however if the signal handler completes without throwing an exception, it will resume the blocking operation
A thread can be killed by application code. A killed thread will notice it has been killed at its next interrupt point, execute any ensure clauses that are applicable, and exit. Effectively, when at the interrupt point it raises a special type of exception that cannot be caught.
The main thread can't be killed - rather, if application code issues a kill instruction on the main thread it executes the default implementation of Kernel.exit(0).
A deadlock occurs when all threads enter certain eligible blocking calls ("deadlocking" calls) which wait for other threads to do something, but because all threads are blocked, none of the threads will ever achieve the conditions for unblock (short of an OS signal being sent to the Ruby program). Eligible calls include:
- Thread joins
- Mutex locks
- Certain unlimited-time sleep calls (e.g.
Thread.stopbut notKernel.sleep)
If all living threads enter such blocking states (or all threads which are not in such a state terminate), Ruby will detect it and raise a deadlock detected exception on the main thread, whose class is an anonymous subclass of Exception.
Ruby's Fibers work as follows:
- Similar to Threads, they implement cooperative multi-threading, however the application code controls if and when the running fiber will yield to another fiber
- Fibers are associated with a specific thread. Each thread has at least one fiber, and fibers can't yield to fibers on different threads.
- If a fiber invokes a blocking call, the whole thread will be blocked i.e. other fibers in the thread won't be yielded to just because one fiber blocks
- There are 2 main styles of Fiber use supported:
- Subroutine style - fibers are used like a subroutine that executes successive portions of its code each time it is invoked. A sub-fiber is invoked (either by being created or being yielded to) with arguments. The sub-fiber resumes execution, does some work (possibly including invoking other sub-fibers) and at some point returns a value, yielding to its calling fiber. Note that reentrant invocations of fibers are not supported.
- Coroutine style - fibers are used for application-controlled cooperative multi-threading. Fibers yield to each other, with arguments, and the invoked fiber resumes execution from where it previously left off. There is no hierarchy of calling and called fibers, there are no restrictions on which fibers can yield to which within the same thread, and there is no need for a "return" instruction to yield to a calling fiber. This mode of fiber use is only enabled if the "fiber" extension library is loaded (i.e. run
require 'fiber')
In the underlying implementation, Ruby fibers typically are represented by OS fiber facilities, however if OS facilities are not available, Ruby will implement fibers using a system of setjmp/longjmps and stack saves and restores.
Similar to threads, a fiber has a program which it begins executing when the fiber is created, and when the program is completed the fiber exits. A thread always has at least 1 fiber, which is the root fiber, and its program is the thread's program. Other fibers are created by application code with a designated block or Proc as their program.
For non-root fibers, their block or Proc runs in a slightly altered execution context, similar to how non-main threads are run. For the purposes of the new fiber it is allocated a new special variables region in its local variable context, but the closure on the other local variables is preserved.
When a fiber other than the root fiber completes normally, it will yield to either its calling fiber (if it exists) or the root fiber, passing 1 argument which is the value of the fiber's program's statement block.
Some exceptional conditions can occur on non-root-fiber completion:
- If a calling fiber exists but has terminated, raises a "fiber dead" error on the root fiber
- Otherwise, if the completing fiber completes with an exception, it will immediately raise an exception on the fiber it yields to
When the root fiber completes (either normally or by throwing an exception), the thread completes.
Ruby core and extension libraries can invoke Ruby API functions in a new protect zone which covers all executing code within that fiber until the function returns. Protect zones appear to be designed to isolate Ruby functions from other C code and restrict them from altering the normal flow of execution outside their protect zone - specifically:
- An exception can't propagate outside its protect zone
- You can't switch to a fiber created in a different protect zone
- You can't call a continuation created in a different protect zone
In Ruby core, the main important use of protect zones is on finalizers - each finalizer invocation runs in its own protect zone, so you can't switch between fibers or call continuations created in and out of finalizers or in different finalizers.
Ruby provides a thread-local storage facility which is in fact fiber-local storage.
Continuations provide functionality similar to an exceptionally powerful goto statement. A continuation is like the label in the goto statement idiom - it is an object containing a saved snapshot of the execution state of a fiber. It can be used as follows:
- The continuation can be restored at any point during the execution of its fiber - the current execution state of the fiber will be discarded, and execution will resume from the point after the continuation was saved
- When a continuation is created, some code can be specified that will be run after the continuation is created, but not after the continuation is restored (by passing a block to the continuation creation function)
This is similar to the setjmp/longjmp facility on many OSes, except that this facility is restricted to restoring to a point that is "up the stack", whereas continuations can be used to restore to any point, including points with a totally different stack. Ruby implements continuations with a combination of setjmp/longjmp calls and stack saves/restores.
Continuations form a closure on every local and dynamic variable in the fiber at the time that the continuation was created. That means that all code in the fiber will see the same local/dynamic variables before as after a continuation restore, variable values are preserved across continuation restores, and therefore updates to any local/dynamic variables in the fiber are visible across continuation restores.
Ruby implements a simple Mutex class, which operates in the standard mutex fashion. Ruby Mutexes are:
- Non-recursive - a thread can't re-lock a mutex it is already locking
- Robust - when a thread terminates, it releases the locks it is holding
Ruby Threads don't keep track of which thread created which. However, ThreadGroups allow you to group threads together based on such information:
- Every thread has one ThreadGroup
- Every thread is created in the same ThreadGroup as its parent thread (the main thread is created in the VM default ThreadGroup)
- A thread can be moved to a different thread group
This may be useful if, for example, you are writing a framework that will be invoking other Ruby code that you aren't in control of, but nonetheless you want to manage the execution of that code. You can run the uncontrolled code in a separate thread which is moved to a separate thread group, and then if that code creates any new threads they can all be identified because they are in the same thread group.
You can also enclose a ThreadGroup, which is an irreversible operation meaning that no threads can be moved into or moved out of that ThreadGroup (although they can still be created within). In this way you can disallow uncontrolled code from trying to manage its own ThreadGroups.
undef Thread::<alloc>
Thread's allocator function is undefined, so you can't create Thread objects through any of the generic creation APIs such as Class's new, allocate and Kernel's clone and dup.
Threads must be created through Thread::new, Thread::start or Thread::fork.
Thread::new *<args> <block>
Raises an error if thread creation has been disallowed as part of VM termination.
Allocates a new Thread object and invokes initialize on it, passing along all arguments and the block. At the time of this initialize call, the new Thread object (which will be self in initialize) is a usable Thread object, but it is not yet a living thread. initialize is run in the current thread, not the new one. Once initialize completes, the new Thread object will be a fully initialized and living thread.
If the initialize call has been overridden, it is expected to invoke the default implementation of Thread#initialize (e.g. through a super expression). If it did not do so, an error is raised.
Thread#initialize *<args> <block>
Raises an error if:
- Called without a block
-
selfis already initialized (i.e. this implementation ofThread#initializehas already been invoked onself)
Assign's self's priority and thread group from the currently running thread.
Runs <block> in self's thread, passing along the arguments to initialize.
Returns self.
Thread::start *<args> <block>
Thread::fork *<args> <block>
Allocates a new Thread object and invokes the default implementation of Thread#initialize on it, passing along the arguments and block.
Returns newly created Thread.
Thread#priority
Thread#priority= <priority>
The priority attribute is an integer defaulting to 0. Higher values mean Ruby will try to give the thread more time, lower values mean less time. Ruby tries to multiply the time allocate by 2^<priority>.
When accessed, returns the thread's priority.
When assigned, sets the thread's priority to <priority>, although the set value may be bounded to a Ruby-defined minimum or maximum value. Returns the (possibly bounded) value assigned.
Thread#abort_on_exception
Thread#abort_on_exception= <value>
The abort_on_exception attribute is either true or false. If a thread terminates with a raised exception, and the thread is not the main thread, then if the thread's abort_on_exception attribute is true, the exception will immediately be raised on the main thread.
When accessed, returns the thread's abort_on_exception attribute.
When assigned, sets the thread's abort_on_exception attribute based on the truthiness or falsiness of <value> and returns <value>.
Thread::abort_on_exception
Thread::abort_on_exception= <value>
Similar to Thread#abort_on_exception but is a single VM-wide attribute that applies to all Threads. The abort on exception behaviour at thread termination is enabled if either the VM-wide Thread::abort_on_exception or the thread-specific Thread#abort_on_exception is true.
Thread::stop
Designed to be used when the thread wants to halt execution until another thread decides to wake it up. This is considered a deadlocking sleep by the Ruby VM.
Raises an exception if the current thread is the only living thread.
Goes to sleep permanently.
Returns nil.
Kernel::sleep [ <time> ]
private Kernel#sleep [ <time> ]
Designed to be used when the thread wants to halt execution, temporarily or permanently, for some reason other than purely waiting for another thread to wake it up. Other threads can still wake it up, but this is not considered a deadlocking sleep by the Ruby VM.
<time> is a numeric (not necessarily integral) value specifying number of seconds to sleep for. If <time> is not provided, it's treated as forever.
Goes to sleep for the time specified.
Returns an integer specifying the actual number of seconds slept.
Thread::pass
Immediately yields control to another thread, if possible.
Returns nil.
Thread#join [ <time> ]
Joins to a thread, i.e. the currently running thread will waits for the self Thread of this method invocation to complete execution (if it has not done so already). If <time> is provided, it is the number (not necessarily integral) of seconds to wait for the thread to complete, otherwise it waits forever.
If <time> expired before the thread completed, returns nil. If the thread completed with a raised exception, raises that exception. Otherwise, returns self.
Thread#value
Similar to Thread#join, with no time limit, with the difference that if the thread completes without raising an exception, this method returns the returned value of the thread's program's statement block.
Thread#raise [ <template> ], [ <argument> ], [ <backtrace> ]
While there is an outstanding exception raise or kill instruction on self, or self is not currently processing interrupts as a result of a pending NoMemoryError or SystemStackError, yields control to another thread and retries.
If self has not yet completed executing, creates an exception object as described in Exceptions and Throw and passes it to self for raising at self's next interrupt point.
Returns nil.
Thread#wakeup
If self has completed executing, raises an error.
Wakes up self.
Returns self.
Thread#run
Same as Thread#wakeup except, if self has not yet completed executing, yields control to another thread.
Thread#kill
Thread#terminate
Thread#exit
If self is the main thread (whether currently running or not), executes the default implementation of Kernel.exit() (which raises a SystemExit exception).
If self has already completed executing, or has a kill instruction pending, does nothing. Otherwise, sends a kill instruction to self.
Returns self.
Thread::kill <thread>
Same as default implementation of <thread>.kill.
Thread::exit
Same as default implementation of Thread.current.kill.
Thread::main
Returns the Thread object for the main thread.
Thread::current
Returns the Thread object for the currently running thread.
Thread::list
Creates and returns an Array containing all currently living Thread objects. A currently living thread is one that has begun executing or is ready to begin executing, and has not yet completed executing.
Thread#status
Returns a string describing self's status:
-
'run'ifselfis running or available to run -
'sleep'ifselfis in a blocking call -
'aborting'ifselfhas a kill instruction pending, but has not yet begun kill processing -
nilifselfhas completed with a thrown exception -
falseifselfhas completed otherwise
Thread#alive?
Returns true if self has not yet completed executing, otherwise returns false.
Thread#stop?
Returns true if self has completed executing or is in a blocking call, otherwise returns false.
Thread#backtrace
Constructs and returns a backtrace of self, an Array of Strings describing the current execution stack of self.
Thread#group
Returns the ThreadGroup that self belongs to.
If self has not yet been fully initialized by the default implementation of Thread#initialize, returns nil.
constant ThreadGroup::Default
The Ruby VM's default thread group, which the main thread belongs to at the beginning of execution.
ThreadGroup#add <thread>
Raises an exception if either self or <thread>'s current ThreadGroup is enclosed.
Changes <thread>'s ThreadGroup to self and returns self.
If <thread> has not yet been fully initialized by the default implementation of Thread#initialize, returns nil.
ThreadGroup#list
Creates and returns an Array containing all the living Thread objects whose ThreadGroup is self.
ThreadGroup#enclose
Sets self to be enclosed. Returns self.
ThreadGroup#enclosed?
Returns true if self is enclosed, otherwise returns false.
Fiber#initialize <block>
Creates and returns a new fiber whose program will be the given <block>. Does not begin executing the new fiber.
Fiber#resume *<args>
Switches to the self fiber to execute as a subroutine.
If self is the root fiber or it has a calling fiber (i.e. it has been previously resume'd without yet yielding), raises an error.
If attempting to switch to a fiber on a different thread, raises an error. If attempting to switch to a fiber created in a different protect zone, raises an error. If attempting to switch to a fiber which has completed executing, raises an error.
Sets the calling fiber property of self to the currently running fiber.
Switches the currently running fiber to self. If self has not begun executing yet, invokes its program with <args> as the parameters. Otherwise, returns <args> from the yield, resume or transfer method that caused self to stop executing, with <args> adjusted as follows:
- if
<args>is more than one argument, wraps it in an array - if
<args>is not provided, usesnil
Returns the arguments passed when the currently running fiber resumes execution via resume, yield, transfer.
Fiber::yield *<args>
Returns to the calling fiber from a Fiber#resume call.
Similar to Fiber#resume except:
- No error is raised if the switched-to fiber is the root fiber or has a calling fiber
- If the currently running fiber is the root fiber, raises an error
- The switched-to fiber will be the calling fiber of the currently running fiber, if it exists, otherwise the root fiber
- The calling fiber property of the currently running fiber is cleared
- The calling fiber property of the switched-to fiber is not altered
Fiber#transfer *<args>
Switches to the self fiber to execute as a coroutine. You must require 'fiber' to use this method.
Similar to Fiber#resume except:
- No error is raised if the switched-to fiber is the root fiber or has a calling fiber
- The calling fiber property of the switched-to fiber is not altered
Fiber::current
Returns the currently running fiber. You must require 'fiber' to use this method.
Fiber#alive?
Returns false if self has completed execution, otherwise true. You must require 'fiber' to use this method.
Thread#[]= <key>, <value>
Sets the value of <key>, specified equivalently as a String or Symbol, to be <value> in fiber-local storage for the fiber currently running in self.
Returns the value stored.
Thread#[] <key>
Returns the last value for <key>, specified equivalently as a String or Symbol, that was stored in fiber-local storage for the fiber currently running in self. Returns nil if none exists.
Thread#key? <key>
Returns true if the fiber currently running in self contains a non-nil value for <key> in its fiber-local storage, otherwise returns false.
Thread#keys
Creates and returns an Array of all the keys, in Symbol form, in fiber-local storage for the fiber currently running in self, where the key has a non-nil value associated.
You must require 'continuation' to use this API.
undef Continuation#<alloc>
undef Continuation#new
You may only create a Continuation object via Kernel#callcc.
Kernel::callcc <block>
private Kernel#callcc <block>
Creates a Continuation object containing a snapshot of the execution context at the point within the callcc method call. Invokes <block> with a single argument of the new Continuation object, then return the value of <block>.
If the continuation object is called, its execution state will be restored, and the restored callcc invocation will return with a value as per the arguments provided when the continuation was called.
Continuation#call *<args>
Continuation#[] *<args>
If attempting to call a continuation created in a different fiber, raises an error. If attempting to call a continuation created in a different protect zone, raises an error.
Restores self's execution context and returns a value from the restored callcc invocation as per <args>:
- If no arguments provided, returns
nil - If 1 argument provided, returns that argument
- Otherwise, returns an Array containing all arguments
Mutex#initialize
Returns self.
Mutex#lock
Raises an error if self is already locked by the current thread.
Blocks until a lock on self is successfully acquired, and returns self.
Mutex#unlock
Raises an error if self is not locked, or has been locked by a thread other than the current thread.
Returns self.
Mutex#locked?
Returns true if self is locked, otherwise returns false.
Mutex#try_lock
If self is currently locked, returns false. Otherwise, locks self and returns true.
Mutex#sleep [ <time> ]
Used to release a mutex for a specified period of time, or until the thread is woken up.
Raises an error if self is not locked, or has been locked by a thread other than the current thread.
<time> is a numeric (not necessarily integral) value specifying number of seconds to sleep for. If <time> is not provided or is nil, it's treated as forever.
Releases self, enters a deadlocking sleep for the specified time, then reacquires self (blocking if necessary). The reacquire takes place in an ensure clause - if an exception is raised while sleeping, the reacquire will still happen and then the exception will be propagated.
Returns an integer specifying the actual number of seconds passed in this method call.
A Ruby Language Reference
Copyright © by Michael Hore, 2016.
Introduction to This Document
Ruby Elements
- Classes and Modules
- Methods
- Blocks, Procs and Lambdas
- Execution Context and Closures
- Variables, Constants and Namespaces
- Types and Literals
- Ruby Expressions
- Operators
Syntax Grammar
Exceptions and Throw
Ruby Sourcefiles and Libraries
Multi Threading
Execution and Lifecycle