Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions spec/std/fiber/execution_context/multi_threaded_spec.cr

This file was deleted.

42 changes: 42 additions & 0 deletions spec/std/fiber/execution_context/parallel_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% skip_file unless flag?(:execution_context) %}
require "spec"

describe Fiber::ExecutionContext::Parallel do
it ".new" do
mt = Fiber::ExecutionContext::Parallel.new("test", maximum: 2)
mt.size.should eq(0)
mt.capacity.should eq(2)

expect_raises(ArgumentError, "needs at least one thread") do
Fiber::ExecutionContext::Parallel.new("test", maximum: -1)
end

expect_raises(ArgumentError, "needs at least one thread") do
Fiber::ExecutionContext::Parallel.new("test", maximum: 0)
end

mt = Fiber::ExecutionContext::Parallel.new("test", size: 0..2)
mt.size.should eq(0)
mt.capacity.should eq(2)

mt = Fiber::ExecutionContext::Parallel.new("test", size: ..4)
mt.size.should eq(0)
mt.capacity.should eq(4)

mt = Fiber::ExecutionContext::Parallel.new("test", size: 1..5)
mt.size.should eq(1)
mt.capacity.should eq(5)

mt = Fiber::ExecutionContext::Parallel.new("test", size: 1...5)
mt.size.should eq(1)
mt.capacity.should eq(4)

expect_raises(ArgumentError, "needs at least one thread") do
Fiber::ExecutionContext::Parallel.new("test", size: 0...1)
end

expect_raises(ArgumentError, "invalid range") do
Fiber::ExecutionContext::Parallel.new("test", size: 5..1)
end
end
end
59 changes: 30 additions & 29 deletions src/fiber/execution_context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ require "./execution_context/*"
{% raise "ERROR: execution contexts require the `preview_mt` compilation flag" unless flag?(:preview_mt) || flag?(:docs) %}
{% raise "ERROR: execution contexts require the `execution_context` compilation flag" unless flag?(:execution_context) || flag?(:docs) %}

# An execution context creates and manages a dedicated pool of 1 or more threads
# where fibers can be executed into. Each context manages the rules to run,
# suspend and swap fibers internally.
# An execution context creates and manages a dedicated pool of 1 or more
# schedulers where fibers will be running in. Each context manages the rules to
# run, suspend and swap fibers internally.
#
# EXPERIMENTAL: Execution contexts are an experimental feature, implementing
# [RFC 2](https://github.com/crystal-lang/rfcs/pull/2). It's opt-in and requires
# the compiler flags `-Dpreview_mt -Dexecution_context`.
#
# Applications can create any number of execution contexts in parallel. These
# contexts are isolated but they can communicate with the usual thread-safe
# synchronization primitives (e.g. `Channel`, `Mutex`).
# contexts are isolated but they can communicate with the usual synchronization
# primitives such as `Channel` or `Mutex`.
#
# An execution context groups fibers together. Instead of associating a fiber to
# a specific thread, we associate a fiber to an execution context, abstracting
# which thread(s) they actually run on.
# a specific system thread, we associate a fiber to an execution context,
# abstracting which system thread(s) the fibers will run on.
#
# When spawning a fiber with `::spawn`, it spawns into the execution context of
# the current fiber. Thus child fibers execute in the same context as their
# parent (unless told otherwise).
# the current fiber, so child fibers execute in the same context as their parent
# (unless told otherwise).
#
# Once spawned, a fiber cannot _move_ to another execution context. It always
# resumes in the same execution context.
Expand All @@ -36,18 +36,19 @@ require "./execution_context/*"
# The standard library provides a number of execution context implementations
# for common use cases.
#
# * `ExecutionContext::SingleThreaded`: Fully concurrent with limited
# parallelism. Fibers run concurrently in a single thread and never in parallel.
# They can use simpler and faster synchronization primitives internally (no
# atomics, no thread safety). Communication with fibers in other contexts
# requires thread-safe primitives. A blocking fiber blocks the entire thread and
# all other fibers in the context.
# * `ExecutionContext::MultiThreaded`: Fully concurrent, fully parallel. Fibers
# running in this context can be resumed by any thread in this context. They run
# concurrently and in parallel to each other, in addition to running in parallel
# to any fibers in other contexts. Schedulers steal work from each other. The
# number of threads can grow and shrink dynamically.
# * `ExecutionContext::Isolated`: Single fiber in a single thread without
# * `ExecutionContext::Concurrent`: Fully concurrent with limited parallelism.
# Fibers run concurrently, never in parallel (only one fiber at a time). They
# can use simpler and faster synchronization primitives internally (no atomics,
# limited thread safety). Communication with fibers in other contexts requires
# thread-safe primitives. A blocking fiber blocks the entire thread and all
# other fibers in the context.
# * `ExecutionContext::Parallel`: Fully concurrent, fully parallel. Fibers
# running in this context can be resumed by multiple system threads in this
# context. They run concurrently and in parallel to each other (multiple fibers
# at a time), in addition to running in parallel to any fibers in other
# contexts. Schedulers steal work from each other. The parallelism can grow and
# shrink dynamically.
# * `ExecutionContext::Isolated`: Single fiber in a single system thread without
# concurrency. This is useful for tasks that can block thread execution for a
# long time (e.g. a GUI main loop, a game loop, or CPU heavy computation). The
# event-loop works normally (when the fiber sleeps, it pauses the thread).
Expand All @@ -56,14 +57,14 @@ require "./execution_context/*"
# ## The default execution context
#
# The Crystal runtime starts with a single threaded execution context, available
# in `Fiber::ExecutionContext.default`.
# as `Fiber::ExecutionContext.default`:
#
# ```
# Fiber::ExecutionContext.default.class # => Fiber::ExecutionContext::SingleThreaded
# Fiber::ExecutionContext.default.class # => Fiber::ExecutionContext::Concurrent
# ```
#
# NOTE: The single threaded default context is required for backwards
# compatibility. It may change to a multi-threaded default context in the
# NOTE: The default context is a `Concurrent` context for backwards
# compatibility reasons. It might change to a `Parallel` context in the
# future.
@[Experimental]
module Fiber::ExecutionContext
Expand All @@ -72,17 +73,17 @@ module Fiber::ExecutionContext
# Returns the default `ExecutionContext` for the process, automatically
# started when the program started.
#
# NOTE: The default context is a `SingleThreaded` context for backwards
# compatibility reasons. It may change to a multi-threaded default context in
# the future.
# NOTE: The default context is a `Concurrent` context for backwards
# compatibility reasons. It might change to a `Parallel` context in the
# future.
@[AlwaysInline]
def self.default : ExecutionContext
@@default.not_nil!("expected default execution context to have been setup")
end

# :nodoc:
def self.init_default_context : Nil
@@default = SingleThreaded.default
@@default = Concurrent.default
@@monitor = Monitor.new
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,59 @@ require "./runnables"
require "./scheduler"

module Fiber::ExecutionContext
# A single-threaded execution context which owns a single thread. It's fully
# concurrent with limited parallelism.
# Concurrent-only execution context.
#
# Concurrency is restricted to a single thread. Fibers in the same context
# will never run in parallel to each other but they may still run in parallel
# to fibers running in other contexts (i.e. in another thread).
# Fibers running in the same context can only run concurrently and never in
# parallel to each others. However, they still run in parallel to fibers
# running in other execution contexts.
#
# Fibers can use simpler and faster synchronization primitives between
# themselves (no atomics, no thread safety). Communication with fibers in
# other contexts requires thread-safe primitives.
# Fibers in this context can use simpler and faster synchronization primitives
# between themselves (for example no atomics or thread safety required), but
# data shared with other contexts needs to be protected (e.g. `Mutex`), and
# communication with fibers in other contexts requires safe primitives, for
# example `Channel`.
#
# A blocking fiber blocks the entire thread and all other fibers in the
# context.
class SingleThreaded
# A blocking fiber blocks the entire context, and thus all the
# other fibers in the context.
#
# For example: we can start a concurrent context to run consumer fibers, while
# the default context produces values. Because the consumer fibers will never
# run in parallel and don't yield between reading *result* then writing it, we
# are not required to synchronize accesses to the value:
#
# ```
# require "wait_group"
#
# consumers = Fiber::ExecutionContext::Concurrent.new("consumers")
# channel = Channel(Int32).new(64)
# wg = WaitGroup.new(32)
#
# result = 0
#
# 32.times do
# consumers.spawn do
# while value = channel.receive?
# # safe, but only for this example:
# result = result + value
# end
# ensure
# wg.done
# end
# end
#
# 1024.times { |i| channel.send(i) }
# channel.close
#
# # wait for all workers to be done
# wg.wait
#
# p result # => 523776
# ```
#
# In practice, we still recommended to always protect shared accesses to a
# variable, for example using `Atomic#add` to increment *result* or a `Mutex`
# for more complex operations.
class Concurrent
include ExecutionContext
include ExecutionContext::Scheduler

Expand Down
28 changes: 16 additions & 12 deletions src/fiber/execution_context/isolated.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@ require "./scheduler"
require "../list"

module Fiber::ExecutionContext
# Isolated execution context. Runs a single thread with a single fiber.
# Isolated execution context to run a single fiber.
#
# Concurrency is disabled within the thread: the fiber owns the thread and the
# thread can only run this fiber. Keep in mind that the fiber will still run
# in parallel to other fibers running in other execution contexts.
# Concurrency and parallelism are disabled. The context guarantees that the
# fiber will always run on the same system thread until it terminates; the
# fiber owns the system thread for its whole lifetime.
#
# The fiber can still spawn fibers into other execution contexts. Since it can
# Keep in mind that the fiber will still run in parallel to other fibers
# running in other execution contexts at the same time.
#
# Concurrency is disabled, so an isolated fiber can't spawn fibers into the
# context, but it can spawn fibers into other execution contexts. Since it can
# be inconvenient to pass an execution context around, calls to `::spawn` will
# spawn a fiber into the specified *spawn_context* that defaults to the
# default execution context.
# spawn a fiber into the specified *spawn_context* during initialization,
# which defaults to `Fiber::ExecutionContext.default`.
#
# Isolated fibers can normally communicate with other fibers running in other
# execution contexts using `Channel(T)`, `WaitGroup` or `Mutex` for example.
# They can also execute IO operations or sleep just like any other fiber.
# execution contexts using `Channel`, `WaitGroup` or `Mutex` for example. They
# can also execute `IO` operations or `sleep` just like any other fiber.
#
# Calls that result in waiting (e.g. sleep, or socket read/write) will block
# the thread since there are no other fibers to switch to. This in turn allows
# to call anything that would block the thread without blocking any other
# fiber.
#
# You can for example use an isolated fiber to run a blocking GUI loop,
# transparently forward `::spawn` to the default context, and eventually only
# block the current fiber while waiting for the GUI application to quit:
# For example you can use start an isolated fiber to run a blocking GUI loop,
# transparently forward `::spawn` to the default context, then keep the main
# fiber to wait until the GUI application quit:
#
# ```
# gtk = Fiber::ExecutionContext::Isolated.new("Gtk") do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,57 @@
require "./global_queue"
require "./multi_threaded/scheduler"
require "./parallel/scheduler"

module Fiber::ExecutionContext
# A multi-threaded execution context which owns one or more threads. It's
# fully concurrent and fully parallel.
# Parallel execution context.
#
# Owns multiple threads and starts a scheduler in each one. The number of
# threads is dynamic. Setting the minimum and maximum to the same value will
# start a fixed number of threads.
# Fibers running in the same context run both concurrently and in parallel to each
# others, in addition to the other fibers running in other execution contexts.
#
# Fibers running in this context can be resumed by any thread in the context.
# Fibers can run concurrently and in parallel to each other, in addition to
# running in parallel to any other fiber running in other contexts.
# The context internally keeps a number of fiber schedulers, each scheduler
# being able to start running on a system thread, so multiple schedulers can
# run in parallel. The fibers are resumable by any scheduler in the context,
# they can thus move from one system thread to another at any time.
#
# The actual parallelism is controlled by the execution context. As the need
# for parallelism increases, for example more fibers running longer, the more
# schedulers will start (and thus system threads), as the need decreases, for
# example not enough fibers, the schedulers will pause themselves and
# parallelism will decrease.
#
# For example: we can start a parallel context to run consumer fibers, while
# the default context produces values. Because the consumer fibers can run in
# parallel, we must protect accesses to the shared *value* variable. Running
# the example without `Atomic#add` would produce a different result every
# time!
#
# ```
# mt_context = Fiber::ExecutionContext::MultiThreaded.new("worker-threads", 4)
# require "wait_group"
#
# consumers = Fiber::ExecutionContext::Parallel.new("consumers", 8)
# channel = Channel(Int32).new(64)
# wg = WaitGroup.new(32)
#
# 10.times do
# mt_context.spawn do
# do_something
# result = Atomic.new(0)
#
# 32.times do
# consumers.spawn do
# while value = channel.receive?
# result.add(value)
# end
# ensure
# wg.done
# end
# end
#
# sleep
# 1024.times { |i| channel.send(i) }
# channel.close
#
# # wait for all workers to be done
# wg.wait
#
# p result.get # => 523776
# ```
class MultiThreaded
class Parallel
include ExecutionContext

getter name : String
Expand Down
Loading