Releasing Execution Contexts [Crystal]

yxhuvud1 pts0 comments

Releasing Execution Contexts - The Crystal Programming Language

Skip to content

GitHub Repository

Forum

RSS-Newsfeed

Releasing Execution Contexts

12 Jul 2026

Julien Portalier

Project<br>#multithreading

Two and a half years ago, with the<br>invaluable support from 84codes, we re-examined the multithreading model<br>inherited from Crystal 0.28 (preview MT).

There are different ways to spread an application to multiple CPU cores. So far,<br>preview MT proposed a simple solution, and it worked great for many cases, yet<br>sometimes we need more control over where and how a specific piece of code must<br>run.

Sometimes we need a fiber to own a thread, notably GUI and game loops.

Sometimes we need a set of fibers to run concurrently.

Sometimes we need fibers to scale to as many CPU cores as needed.

Inspired by Kotlin contexts, we realized that we didn’t have to pick just<br>one model. What if we designed an interface instead?

Hence came Execution Contexts . Plural, because there are multiple ways to<br>orchestrate fibers across one to many threads. Ultimately we plan to make the<br>interface public, so you may write your own models.

The default execution context

Section titled The default execution context

Applications run in a default context that is automatically created on the main<br>thread. It’s a parallel context that defaults to a parallelism of 1, so fibers<br>run concurrently on just one thread. This avoids a breaking change to existing<br>applications that may not be ready for MT.

You can resize the default context to increase parallelism, making it truly<br>parallel, and letting it automatically scale fibers across CPU cores as needed<br>at runtime:

Fiber::ExecutionContext.default.resize(maximum: System.cpu_count)

You can keep it concurrent and start additional contexts to control the<br>execution and let the OS preempt the threads:

parallel = Fiber::ExecutionContext::Parallel.new("MT", maximum: 4)<br>parallel.spawn { }

You can keep it single-threaded if you don’t need parallelism, or let users<br>determine the parallelism through a --threads N argument.

Your application, your choice.

What do execution contexts provide?

Section titled What do execution contexts provide?

Execution contexts allow you to start one or many fiber orchestrators to run<br>fibers in different manners. Fibers are tied to their execution context, and the<br>execution of fibers depends on the context they belong to.

We currently provide three different context types:

Concurrent : Fibers spawned into the context run concurrently to each<br>other, and will never run in parallel; they only run in parallel to fibers<br>running in other contexts.

Parallel : Fibers spawned in a parallel context run concurrently and in<br>parallel to each other, in addition to fibers running in other contexts. The<br>context automatically scales to multiple CPU cores, up to the configured<br>maximum.

Isolated : Spawn a single fiber to a system thread. The fiber owns the<br>thread for its whole lifetime. The fiber can block the thread however it wants<br>(it owns it) with no impact on the rest of the application.

Can execution contexts communicate?

Section titled Can execution contexts communicate?

Fibers can always communicate and synchronize with any other fibers, regardless<br>of the execution context in which they run. Use Channel and Sync types<br>normally.

Note that cross context communication requires more synchronization than<br>internal communication, and can thus be slower. This is mostly noticeable in<br>extreme situations, notably benchmarks.

How are execution contexts different from the ‘preview MT’ model?

Section titled How are execution contexts different from the ‘preview MT’ model?

Preview MT starts a fixed number of threads and ties each fiber to a single<br>thread on which it is always resumed. You have no control over where a fiber<br>would start aside from “spawn on the current thread of the current fiber”; you<br>can’t isolate a fiber to a thread, plus other limitations.

Fibers can get stuck on one thread busy running a CPU-intensive work, while<br>other threads are idle. A slow getaddrinfo DNS request, for example, might<br>block your whole application from making any progress, or event fail to respond<br>to Ctrl+C or SIGINT to terminate the process.

Execution contexts solve all these issues.

What changes?

Section titled What changes?

The fiber scheduler has seen a complete overhaul. It is nothing like before. Not<br>only is it faster than the legacy schedulers, including both single thread and<br>preview MT, fibers will now automatically scale to as many CPU cores as needed<br>at runtime.

The CRYSTAL_WORKERS environment variable

Section titled The CRYSTAL_WORKERS environment variable

The CRYSTAL_WORKERS environment variable is no longer used by default. You can<br>use it manually to resize the default context, or start a parallel context. For<br>example:

maximum = ENV["CRYSTAL_WORKERS"]?.try(&.to_i?) || 4<br>Fiber::ExecutionContext.default.resize(maximum)

Breaking changes

Section titled Breaking...

execution contexts fibers context fiber thread

Related Articles