diff --git a/spec/std/fiber/execution_context/multi_threaded_spec.cr b/spec/std/fiber/execution_context/multi_threaded_spec.cr deleted file mode 100644 index 96b2b9e23274..000000000000 --- a/spec/std/fiber/execution_context/multi_threaded_spec.cr +++ /dev/null @@ -1,42 +0,0 @@ -{% skip_file unless flag?(:execution_context) %} -require "spec" - -describe Fiber::ExecutionContext::MultiThreaded do - it ".new" do - mt = Fiber::ExecutionContext::MultiThreaded.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::MultiThreaded.new("test", maximum: -1) - end - - expect_raises(ArgumentError, "needs at least one thread") do - Fiber::ExecutionContext::MultiThreaded.new("test", maximum: 0) - end - - mt = Fiber::ExecutionContext::MultiThreaded.new("test", size: 0..2) - mt.size.should eq(0) - mt.capacity.should eq(2) - - mt = Fiber::ExecutionContext::MultiThreaded.new("test", size: ..4) - mt.size.should eq(0) - mt.capacity.should eq(4) - - mt = Fiber::ExecutionContext::MultiThreaded.new("test", size: 1..5) - mt.size.should eq(1) - mt.capacity.should eq(5) - - mt = Fiber::ExecutionContext::MultiThreaded.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::MultiThreaded.new("test", size: 0...1) - end - - expect_raises(ArgumentError, "invalid range") do - Fiber::ExecutionContext::MultiThreaded.new("test", size: 5..1) - end - end -end diff --git a/spec/std/fiber/execution_context/parallel_spec.cr b/spec/std/fiber/execution_context/parallel_spec.cr new file mode 100644 index 000000000000..cb358577f453 --- /dev/null +++ b/spec/std/fiber/execution_context/parallel_spec.cr @@ -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 diff --git a/src/fiber/execution_context.cr b/src/fiber/execution_context.cr index 8d2289740ebc..7208a1b5bd10 100644 --- a/src/fiber/execution_context.cr +++ b/src/fiber/execution_context.cr @@ -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. @@ -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). @@ -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 @@ -72,9 +73,9 @@ 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") @@ -82,7 +83,7 @@ module Fiber::ExecutionContext # :nodoc: def self.init_default_context : Nil - @@default = SingleThreaded.default + @@default = Concurrent.default @@monitor = Monitor.new end diff --git a/src/fiber/execution_context/single_threaded.cr b/src/fiber/execution_context/concurrent.cr similarity index 79% rename from src/fiber/execution_context/single_threaded.cr rename to src/fiber/execution_context/concurrent.cr index f4a2aa4f3694..72db0b7eb125 100644 --- a/src/fiber/execution_context/single_threaded.cr +++ b/src/fiber/execution_context/concurrent.cr @@ -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 @@ -279,4 +318,7 @@ module Fiber::ExecutionContext end end end + + @[Deprecated("Use Fiber::ExecutionContext::Concurrent instead.")] + alias SingleThreaded = Concurrent end diff --git a/src/fiber/execution_context/isolated.cr b/src/fiber/execution_context/isolated.cr index 566356d62316..b9a7021d7757 100644 --- a/src/fiber/execution_context/isolated.cr +++ b/src/fiber/execution_context/isolated.cr @@ -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 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 diff --git a/src/fiber/execution_context/multi_threaded.cr b/src/fiber/execution_context/parallel.cr similarity index 83% rename from src/fiber/execution_context/multi_threaded.cr rename to src/fiber/execution_context/parallel.cr index e843178d1d10..2e041003936f 100644 --- a/src/fiber/execution_context/multi_threaded.cr +++ b/src/fiber/execution_context/parallel.cr @@ -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) + # + # result = Atomic.new(0) # - # 10.times do - # mt_context.spawn do - # do_something + # 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 @@ -314,4 +341,7 @@ module Fiber::ExecutionContext io << ' ' << name << '>' end end + + @[Deprecated("Use Fiber::ExecutionContext::Parallel instead.")] + alias MultiThreaded = Parallel end diff --git a/src/fiber/execution_context/multi_threaded/scheduler.cr b/src/fiber/execution_context/parallel/scheduler.cr similarity index 91% rename from src/fiber/execution_context/multi_threaded/scheduler.cr rename to src/fiber/execution_context/parallel/scheduler.cr index 5aadffaaa376..7268f5afe5c9 100644 --- a/src/fiber/execution_context/multi_threaded/scheduler.cr +++ b/src/fiber/execution_context/parallel/scheduler.cr @@ -3,17 +3,21 @@ require "../scheduler" require "../runnables" module Fiber::ExecutionContext - class MultiThreaded - # MT fiber scheduler. + class Parallel + # Individual scheduler for the parallel execution context. # - # Owns a single thread inside a MT execution context. + # The execution context itself doesn't run the fibers. The fibers actually + # run in the schedulers. Each scheduler in the context increases the + # parallelism by one. For example a parallel context with 8 schedulers means + # that a maximum of 8 fibers may run at the same time in different system + # threads. class Scheduler include ExecutionContext::Scheduler getter name : String # :nodoc: - property execution_context : MultiThreaded + property execution_context : Parallel protected property! thread : Thread protected property! main_fiber : Fiber @@ -38,10 +42,9 @@ module Fiber::ExecutionContext self.spawn(name: name, &block) end - # Unlike `ExecutionContext::MultiThreaded#enqueue` this method is only - # safe to call on `ExecutionContext.current` which should always be the - # case, since cross context enqueues must call - # `ExecutionContext::MultiThreaded#enqueue` through `Fiber#enqueue`. + # Unlike `Parallel#enqueue` this method is only safe to call on + # `ExecutionContext.current` which should always be the case, since cross + # context enqueues must call `Parallel#enqueue` through `Fiber#enqueue`. protected def enqueue(fiber : Fiber) : Nil Crystal.trace :sched, "enqueue", fiber: fiber @runnables.push(fiber) @@ -183,7 +186,7 @@ module Fiber::ExecutionContext # immediately mark the scheduler as spinning (we just unparked); we # don't increment the number of spinning threads since - # `MultiThreaded#wake_scheduler` already did + # `Parallel#wake_scheduler` already did @spinning = true end