Skip to content

Commit fcd9d60

Browse files
committed
Rename execution contexts + improve documentation
1 parent 08940fc commit fcd9d60

File tree

7 files changed

+193
-119
lines changed

7 files changed

+193
-119
lines changed

spec/std/fiber/execution_context/multi_threaded_spec.cr

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% skip_file unless flag?(:execution_context) %}
2+
require "spec"
3+
4+
describe Fiber::ExecutionContext::Parallel do
5+
it ".new" do
6+
mt = Fiber::ExecutionContext::Parallel.new("test", maximum: 2)
7+
mt.size.should eq(0)
8+
mt.capacity.should eq(2)
9+
10+
expect_raises(ArgumentError, "needs at least one thread") do
11+
Fiber::ExecutionContext::Parallel.new("test", maximum: -1)
12+
end
13+
14+
expect_raises(ArgumentError, "needs at least one thread") do
15+
Fiber::ExecutionContext::Parallel.new("test", maximum: 0)
16+
end
17+
18+
mt = Fiber::ExecutionContext::Parallel.new("test", size: 0..2)
19+
mt.size.should eq(0)
20+
mt.capacity.should eq(2)
21+
22+
mt = Fiber::ExecutionContext::Parallel.new("test", size: ..4)
23+
mt.size.should eq(0)
24+
mt.capacity.should eq(4)
25+
26+
mt = Fiber::ExecutionContext::Parallel.new("test", size: 1..5)
27+
mt.size.should eq(1)
28+
mt.capacity.should eq(5)
29+
30+
mt = Fiber::ExecutionContext::Parallel.new("test", size: 1...5)
31+
mt.size.should eq(1)
32+
mt.capacity.should eq(4)
33+
34+
expect_raises(ArgumentError, "needs at least one thread") do
35+
Fiber::ExecutionContext::Parallel.new("test", size: 0...1)
36+
end
37+
38+
expect_raises(ArgumentError, "invalid range") do
39+
Fiber::ExecutionContext::Parallel.new("test", size: 5..1)
40+
end
41+
end
42+
end

src/fiber/execution_context.cr

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ require "./execution_context/*"
88
{% raise "ERROR: execution contexts require the `preview_mt` compilation flag" unless flag?(:preview_mt) || flag?(:docs) %}
99
{% raise "ERROR: execution contexts require the `execution_context` compilation flag" unless flag?(:execution_context) || flag?(:docs) %}
1010

11-
# An execution context creates and manages a dedicated pool of 1 or more threads
12-
# where fibers can be executed into. Each context manages the rules to run,
13-
# suspend and swap fibers internally.
11+
# An execution context creates and manages a dedicated pool of 1 or more
12+
# schedulers where fibers will be running in. Each context manages the rules to
13+
# run, suspend and swap fibers internally.
1414
#
1515
# EXPERIMENTAL: Execution contexts are an experimental feature, implementing
1616
# [RFC 2](https://github.com/crystal-lang/rfcs/pull/2). It's opt-in and requires
1717
# the compiler flags `-Dpreview_mt -Dexecution_context`.
1818
#
1919
# Applications can create any number of execution contexts in parallel. These
20-
# contexts are isolated but they can communicate with the usual thread-safe
21-
# synchronization primitives (e.g. `Channel`, `Mutex`).
20+
# contexts are isolated but they can communicate with the usual synchronization
21+
# primitives such as `Channel` or `Mutex`.
2222
#
2323
# An execution context groups fibers together. Instead of associating a fiber to
24-
# a specific thread, we associate a fiber to an execution context, abstracting
25-
# which thread(s) they actually run on.
24+
# a specific system thread, we associate a fiber to an execution context,
25+
# abstracting which system thread(s) the fibers will run on.
2626
#
2727
# When spawning a fiber with `::spawn`, it spawns into the execution context of
28-
# the current fiber. Thus child fibers execute in the same context as their
29-
# parent (unless told otherwise).
28+
# the current fiber, so child fibers execute in the same context as their parent
29+
# (unless told otherwise).
3030
#
3131
# Once spawned, a fiber cannot _move_ to another execution context. It always
3232
# resumes in the same execution context.
@@ -36,18 +36,19 @@ require "./execution_context/*"
3636
# The standard library provides a number of execution context implementations
3737
# for common use cases.
3838
#
39-
# * `ExecutionContext::SingleThreaded`: Fully concurrent with limited
40-
# parallelism. Fibers run concurrently in a single thread and never in parallel.
41-
# They can use simpler and faster synchronization primitives internally (no
42-
# atomics, no thread safety). Communication with fibers in other contexts
43-
# requires thread-safe primitives. A blocking fiber blocks the entire thread and
44-
# all other fibers in the context.
45-
# * `ExecutionContext::MultiThreaded`: Fully concurrent, fully parallel. Fibers
46-
# running in this context can be resumed by any thread in this context. They run
47-
# concurrently and in parallel to each other, in addition to running in parallel
48-
# to any fibers in other contexts. Schedulers steal work from each other. The
49-
# number of threads can grow and shrink dynamically.
50-
# * `ExecutionContext::Isolated`: Single fiber in a single thread without
39+
# * `ExecutionContext::Concurrent`: Fully concurrent with limited parallelism.
40+
# Fibers run concurrently, never in parallel (only one fiber at a time). They
41+
# can use simpler and faster synchronization primitives internally (no atomics,
42+
# limited thread safety). Communication with fibers in other contexts requires
43+
# thread-safe primitives. A blocking fiber blocks the entire thread and all
44+
# other fibers in the context.
45+
# * `ExecutionContext::Parallel`: Fully concurrent, fully parallel. Fibers
46+
# running in this context can be resumed by multiple system threads in this
47+
# context. They run concurrently and in parallel to each other (multiple fibers
48+
# at a time), in addition to running in parallel to any fibers in other
49+
# contexts. Schedulers steal work from each other. The parallelism can grow and
50+
# shrink dynamically.
51+
# * `ExecutionContext::Isolated`: Single fiber in a single system thread without
5152
# concurrency. This is useful for tasks that can block thread execution for a
5253
# long time (e.g. a GUI main loop, a game loop, or CPU heavy computation). The
5354
# event-loop works normally (when the fiber sleeps, it pauses the thread).
@@ -56,33 +57,33 @@ require "./execution_context/*"
5657
# ## The default execution context
5758
#
5859
# The Crystal runtime starts with a single threaded execution context, available
59-
# in `Fiber::ExecutionContext.default`.
60+
# as `Fiber::ExecutionContext.default`:
6061
#
6162
# ```
62-
# Fiber::ExecutionContext.default.class # => Fiber::ExecutionContext::SingleThreaded
63+
# Fiber::ExecutionContext.default.class # => Fiber::ExecutionContext::Concurrent
6364
# ```
6465
#
65-
# NOTE: The single threaded default context is required for backwards
66-
# compatibility. It may change to a multi-threaded default context in the
67-
# future.
66+
# NOTE: The default context being single threaded is required for backwards
67+
# compatibility. It might change to become a multi-threaded default context in
68+
# the future.
6869
@[Experimental]
6970
module Fiber::ExecutionContext
7071
@@default : ExecutionContext?
7172

7273
# Returns the default `ExecutionContext` for the process, automatically
7374
# started when the program started.
7475
#
75-
# NOTE: The default context is a `SingleThreaded` context for backwards
76-
# compatibility reasons. It may change to a multi-threaded default context in
77-
# the future.
76+
# NOTE: The default context is a `Concurrent` context for backwards
77+
# compatibility reasons. It might change to a `Parallel` context in the
78+
# future.
7879
@[AlwaysInline]
7980
def self.default : ExecutionContext
8081
@@default.not_nil!("expected default execution context to have been setup")
8182
end
8283

8384
# :nodoc:
8485
def self.init_default_context : Nil
85-
@@default = SingleThreaded.default
86+
@@default = Concurrent.default
8687
@@monitor = Monitor.new
8788
end
8889

src/fiber/execution_context/single_threaded.cr renamed to src/fiber/execution_context/concurrent.cr

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,59 @@ require "./runnables"
33
require "./scheduler"
44

55
module Fiber::ExecutionContext
6-
# A single-threaded execution context which owns a single thread. It's fully
7-
# concurrent with limited parallelism.
6+
# Concurrent-only execution context.
87
#
9-
# Concurrency is restricted to a single thread. Fibers in the same context
10-
# will never run in parallel to each other but they may still run in parallel
11-
# to fibers running in other contexts (i.e. in another thread).
8+
# Fibers running in this context can only run concurrently and never in
9+
# parallel to each others. However, they still run in parallel to fibers
10+
# running in other execution contexts.
1211
#
13-
# Fibers can use simpler and faster synchronization primitives between
14-
# themselves (no atomics, no thread safety). Communication with fibers in
15-
# other contexts requires thread-safe primitives.
12+
# Fibers in this context can use simpler and faster synchronization primitives
13+
# between themselves (for example no atomics or thread safety required), but
14+
# data shared with other contexts needs to be protected (e.g. `Mutex`), and
15+
# communication with fibers in other contexts requires safe primitives, for
16+
# example `Channel`.
1617
#
17-
# A blocking fiber blocks the entire thread and all other fibers in the
18-
# context.
19-
class SingleThreaded
18+
# A blocking fiber blocks will block the entire context, and thus all the
19+
# other fibers in the context.
20+
#
21+
# For example: we can start a concurrent context to run consumer fibers, while
22+
# the default context produces values. Because the consumer fibers will never
23+
# run in parallel and don't yield between reading *result* then writing it, we
24+
# are not required to synchronize accesses to the value:
25+
#
26+
# ```
27+
# require "wait_group"
28+
#
29+
# consumers = Fiber::ExecutionContext::Concurrent.new("consumers")
30+
# channel = Channel(Int32).new(64)
31+
# wg = WaitGroup.new(32)
32+
#
33+
# result = 0
34+
#
35+
# 32.times do
36+
# consumers.spawn do
37+
# while value = channel.receive?
38+
# # safe, but only for this example:
39+
# result = result + value
40+
# end
41+
# ensure
42+
# wg.done
43+
# end
44+
# end
45+
#
46+
# 1024.times { |i| channel.send(i) }
47+
# channel.close
48+
#
49+
# # wait for all workers to be done
50+
# wg.wait
51+
#
52+
# p result # => 523776
53+
# ```
54+
#
55+
# In practice, we still recommended to always protect shared accesses to a
56+
# variable, for example using `Atomic#add` to increment *result* or a `Mutex`
57+
# for more complex operations.
58+
class Concurrent
2059
include ExecutionContext
2160
include ExecutionContext::Scheduler
2261

src/fiber/execution_context/isolated.cr

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,33 @@ require "./scheduler"
22
require "../list"
33

44
module Fiber::ExecutionContext
5-
# Isolated execution context. Runs a single thread with a single fiber.
5+
# Isolated execution context to run a single fiber.
66
#
7-
# Concurrency is disabled within the thread: the fiber owns the thread and the
8-
# thread can only run this fiber. Keep in mind that the fiber will still run
9-
# in parallel to other fibers running in other execution contexts.
7+
# Concurrency and parallelism are disabled. The context guarantees that the
8+
# fiber will always run on the same system thread until it terminates; the
9+
# fiber owns the system thread for its whole lifetime.
1010
#
11-
# The fiber can still spawn fibers into other execution contexts. Since it can
11+
# Keep in mind that the fiber will still run in parallel to other fibers
12+
# running in other execution contexts at the same time.
13+
#
14+
# Concurrency is disabled, so an an isolated fiber can't spawn fibers into the
15+
# context, but it can spawn fibers into other execution contexts. Since it can
1216
# be inconvenient to pass an execution context around, calls to `::spawn` will
13-
# spawn a fiber into the specified *spawn_context* that defaults to the
14-
# default execution context.
17+
# spawn a fiber into the specified *spawn_context* during initialization,
18+
# which defaults to `Fiber::ExecutionContext.default`.
1519
#
1620
# Isolated fibers can normally communicate with other fibers running in other
17-
# execution contexts using `Channel(T)`, `WaitGroup` or `Mutex` for example.
18-
# They can also execute IO operations or sleep just like any other fiber.
21+
# execution contexts using `Channel`, `WaitGroup` or `Mutex` for example. They
22+
# can also execute `IO` operations or `sleep` just like any other fiber.
1923
#
2024
# Calls that result in waiting (e.g. sleep, or socket read/write) will block
2125
# the thread since there are no other fibers to switch to. This in turn allows
2226
# to call anything that would block the thread without blocking any other
2327
# fiber.
2428
#
25-
# You can for example use an isolated fiber to run a blocking GUI loop,
26-
# transparently forward `::spawn` to the default context, and eventually only
27-
# block the current fiber while waiting for the GUI application to quit:
29+
# For example you can use start an isolated fiber to run a blocking GUI loop,
30+
# transparently forward `::spawn` to the default context, then keep the main
31+
# fiber to wait until the GUI application quit:
2832
#
2933
# ```
3034
# gtk = Fiber::ExecutionContext::Isolated.new("Gtk") do

src/fiber/execution_context/multi_threaded.cr renamed to src/fiber/execution_context/parallel.cr

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,57 @@
11
require "./global_queue"
2-
require "./multi_threaded/scheduler"
2+
require "./parallel/scheduler"
33

44
module Fiber::ExecutionContext
5-
# A multi-threaded execution context which owns one or more threads. It's
6-
# fully concurrent and fully parallel.
5+
# Parallel execution context.
76
#
8-
# Owns multiple threads and starts a scheduler in each one. The number of
9-
# threads is dynamic. Setting the minimum and maximum to the same value will
10-
# start a fixed number of threads.
7+
# Fibers running in this context run both concurrently and in parallel to each
8+
# others, in addition to the other fibers running in other execution contexts.
119
#
12-
# Fibers running in this context can be resumed by any thread in the context.
13-
# Fibers can run concurrently and in parallel to each other, in addition to
14-
# running in parallel to any other fiber running in other contexts.
10+
# The context internally keeps a number of fiber schedulers, each scheduler
11+
# being able to start running on a system thread, so multiple schedulers can
12+
# run in parallel. The fibers are resumable by any scheduler in the context,
13+
# they can thus move from one system thread to another at any time.
14+
#
15+
# The actual parallelism is controlled by the execution context. As the need
16+
# for parallelism increases, for example more fibers running longer, the more
17+
# schedulers will start (and thus system threads), as the need decreases, for
18+
# example not enough fibers, the schedulers will pause themselves and
19+
# parallelism will decrease.
20+
#
21+
# For example: we can start a parallel context to run consumer fibers, while
22+
# the default context produces values. Because the consumer fibers can run in
23+
# parallel, we must protect accesses to the shared *value* variable. Running
24+
# the example without `Atomic#add` would produce a different result every
25+
# time!
1526
#
1627
# ```
17-
# mt_context = Fiber::ExecutionContext::MultiThreaded.new("worker-threads", 4)
28+
# require "wait_group"
29+
#
30+
# consumers = Fiber::ExecutionContext::Parallel.new("consumers", 8)
31+
# channel = Channel(Int32).new(64)
32+
# wg = WaitGroup.new(32)
1833
#
19-
# 10.times do
20-
# mt_context.spawn do
21-
# do_something
34+
# result = Atomic.new(0)
35+
#
36+
# 32.times do
37+
# consumers.spawn do
38+
# while value = channel.receive?
39+
# result.add(value)
40+
# end
41+
# ensure
42+
# wg.done
2243
# end
2344
# end
2445
#
25-
# sleep
46+
# 1024.times { |i| channel.send(i) }
47+
# channel.close
48+
#
49+
# # wait for all workers to be done
50+
# wg.wait
51+
#
52+
# p result.get # => 523776
2653
# ```
27-
class MultiThreaded
54+
class Parallel
2855
include ExecutionContext
2956

3057
getter name : String

0 commit comments

Comments
 (0)