# How to add command line options to specify the number of threads?

**URL:** <https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286>\
**Category:** Help & Support\
**Created:** [January 9, 2024, 3:18pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286 "2024-01-09T15:18:02Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![kojix2](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/kojix2/32/1583_2.png) [@kojix2](https://forum.crystal-lang.org/u/kojix2)\
**Post date:** [January 9, 2024, 3:18pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/1 "2024-01-09T15:18:02Z")

</div>

I am creating a command line tool.  
In this tool, I want to add a command line option to set the number of threads.

At the moment, I have something like this in mind.

First, add `add_worker` and `remove_worker` to `Crystal::Scheduler`.

```cr
class Crystal::Scheduler
  def self.add_worker
    pending = Atomic(Int32).new(1)
    th = Thread.new do
        scheduler = Thread.current.scheduler
        pending.sub(1)
        scheduler.run_loop
      end
    @@workers.not_nil! << th
    while pending.get > 0
      Fiber.yield
    end
  end

  def self.remove_worker
    return if @@workers.not_nil!.size <= 1
    @@workers.not_nil!.pop
  end
end

```

Next, write a option parser.

```cr
on("-t", "--threads INT", "Number of threads [4]") { |v| set_threads(v.to_i) }

```

```cr
private def set_threads(n)
  if n > 4
    (n - 4).times { Crystal::Scheduler.add_worker }
  elsif n < 4 && n > 0
    (4 - n).times { Crystal::Scheduler.remove_worker }
  else
    Utils.print_error!("Invalid number of threads: #{n}")
  end
end

```

This somehow works (or appears to work) But not reliable.

How would you implement this feature?

@@workers is generated here

> <https://github.com/crystal-lang/crystal/blob/95d04fab447c48d7cb94f5937e52f6556e3ff858/src/crystal/scheduler.cr#L245C1-L257C12>

It may be easy to avoid assigning jobs to some workers, but that will not increase the number of workers.

> <https://github.com/crystal-lang/crystal/blob/95d04fab447c48d7cb94f5937e52f6556e3ff858/src/crystal/scheduler.cr#L200C1-L207C8>

I know it is dangerous to change the number of working members during execution, I want to set it only once when I start the command (in options, not in environment variables).

---

<div class="post-metadata">

**Author:** ![MistressRemilia](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/mistressremilia/32/2585_2.png) [@MistressRemilia](https://forum.crystal-lang.org/u/MistressRemilia)\
**Post date:** [January 9, 2024, 7:39pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/2 "2024-01-09T19:39:59Z")

</div>

Aside from the usual `CRYSTAL_WORKERS` environment variable, this is how I did it with Benben (“–jobs” controls both the worker threads and number of fibers spawned in Benben): [kludge](https://chiselapp.com/user/MistressRemilia/repository/benben/file?ci=46e5d48b28407b30967e43d0d69787b112059c733a36fe4dab477f3a7b9ece52&name=src/main.cr&ln=253). I’ve confirmed it to work, though I’m not particularly happy with how hacky it feels.

Note you don’t have everything at your disposal that early, so you may need to use LibC for a few things like I did.

---

<div class="post-metadata">

**Author:** ![yxhuvud](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/yxhuvud/32/27_2.png) [@yxhuvud](https://forum.crystal-lang.org/u/yxhuvud)\
**Post date:** [January 10, 2024, 8:06pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/3 "2024-01-10T20:06:17Z")

</div>

You can perhaps use my library, [GitHub - yxhuvud/nested\_scheduler: Shard for creating separate groups of fibers in a hierarchical way and to collect results and errors in a structured way.](https://github.com/yxhuvud/nested_scheduler) that provides this functionality (achieved by MONKEYPATCHING ALL OVER THE INNARDS OF THE LANGUAGE IMPLEMENTATION). Be aware that I havn’t checked if it works for 1.11 yet, I won’t have time to do that before the weekend. It tends to break with new crystal versions due to it not respecting the private boundaries of the implementation, and that is fine with me, but consider yourself warned. It can and will break.

But please file issues if it breaks - earlier this year it took a couple of months before I noticed it was broken.

---

<div class="post-metadata">

**Author:** ![zw963](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/zw963/32/1623_2.png) [@zw963](https://forum.crystal-lang.org/u/zw963)\
**Post date:** [January 14, 2024, 10:00am UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/4 "2024-01-14T10:00:14Z")

</div>

Definitely your all experts, unfortunately, as far as I know, many people like you, , are not interested in contributing to Crystal itself, but instead create new shards for some reasons.

---

<div class="post-metadata">

**Author:** ![zw963](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/zw963/32/1623_2.png) [@zw963](https://forum.crystal-lang.org/u/zw963)\
**Post date:** [January 14, 2024, 10:08am UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/5 "2024-01-14T10:08:30Z")

</div>

> WARNING: `nested_scheduler` replaces the built in scheduler with itself, which means that PROGRAMS THAT SPAWN FIBERS WILL NOT EXIT UNTIL ALL FIBERS HAVE STOPPED RUNNING. This is in general a very good thing, but it may be disruptive for programs not built assuming that.

I thought this really a good things, don’t know if there is any discussion about this.

* * *

Hi, i read through the README, but still have confusion, Does the following code will create a new thread for each `pool.spawn { ... }`? when use -Dpreview\_mt?

```crystal
NestedScheduler::ThreadPool.nursery do |pool|
  pool.spawn { sleep 5 }
  pool.spawn { sleep 2 }
end

```

Or should

```crystal
NestedScheduler::ThreadPool.nursery(thread_count: 2) do |pool|
  pool.spawn { sleep 5 }
  pool.spawn { sleep 2 }
end

```

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [January 14, 2024, 12:01pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/6 "2024-01-14T12:01:00Z")

</div>

I don’t think you’re doing justice here. Both @yxhuvud and @MistressRemilia have made valuable contributions to Crystal in the past.

The thing is, the path to getting a change into a large project like Crystal has some intentional obstacles. They ensure changes are reasonable and benefitial for the language and community.  
You need to cover all kinds of details, edge cases and compatibility considerations, making a convincing argument to gain Core Team approval. Especially for complex and integral components such as the scheduler, this is a very difficult task.  
Launching an independent shard has much less resistance because you’re calling the shots. There’s no need to consider a broad range of existing use cases. This is great for getting an idea out and see how it works in practice. I think this is in fact very valuable for gathering information, which may lead to getting things merged upstream eventually.

---

<div class="post-metadata">

**Author:** ![yxhuvud](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/yxhuvud/32/27_2.png) [@yxhuvud](https://forum.crystal-lang.org/u/yxhuvud)\
**Post date:** [January 14, 2024, 12:39pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/7 "2024-01-14T12:39:21Z")

</div>

> I thought this really a good things, don’t know if there is any discussion about this.

About things not exiting before fibers are done, see [Notes on structured concurrency, or: Go statement considered harmful — njs blog](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/) . Well worth a read and it basically redefined how I thought about concurrency back when it was posted.

> Does the following code will create a new thread for each `pool.spawn`

No. The first example will create a dedicated thread to execute ALL fibers spawned in the pool in that single thread, and the second will create 2 threads dedicated to executing them. How many times you spawn does not impact the amount of threads created.

Regarding

> Definitely your all experts, unfortunately, as far as I know, many people like you, , are not interested in contributing to Crystal itself, but instead create new shards for some reasons.

So the thing is, the shard is mainly an accident. It is not at all what I set out to build - if it was a serious try at starting at [[RFC] Structured Concurrency · Issue #6468 · crystal-lang/crystal · GitHub](https://github.com/crystal-lang/crystal/issues/6468) , then it would have started in the other end, with building the thing that is missing in the current implementation - nurseries that don’t spawn new thread pools but instead work in an existing thread pool. It should by a big margin be the most common way to use it.

So why did I start in the wrong end? Well, because I had to. The problem I was aiming to solve was different, namely I wanted to have a way to have a pluggable event machine to drive crystal. And that is the main feature of nested\_scheduler even if it is not documented. It is also probably not something Crystal upstream will want to have, even if the work to get there is pretty low once it is possible to set up custom pools. But in any case, it turned out that it is quite useful to be able to spin up independent thread pools on the fly, so I extracted those parts into a shard and put it out there.

So why have I not tried to upstream it. Well, as I mentioned, it is lacking the most central feature in that it doesn’t support creating nurseries without creating new threads. Additionally, there are other features that are missing in crystal scheduling/threading, and one of those are work stealing. The current way to do round-robin and then have the fiber locked to whatever thread it ended up to is quite limiting and creates a lot of issues. So it also needs to be solved, and having a variable amount of threads around will without doubt make things a lot more complex to reason about. Getting all that to work together will be a huge challenge and it would be good to have at least an idea of what implementation is wanted before starting to upstream stuff

---

<div class="post-metadata">

**Author:** ![zw963](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/zw963/32/1623_2.png) [@zw963](https://forum.crystal-lang.org/u/zw963)\
**Post date:** [January 14, 2024, 2:34pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/8 "2024-01-14T14:34:38Z")

</div>

> About things not exiting before fibers are done, see [Notes on structured concurrency, or: Go statement considered harmful — njs blog](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/) .

Cool, will read it carefully.

> [@yxhuvud](#):
>
> have a pluggable event machine to drive crystal.

Limited by knowledge, don’t understand what you means.

> No. The first example will create a dedicated thread to execute ALL fibers spawned in the pool in that single thread

Well, if this is true, what is the difference when add more than one `pool.spawn`?

e.g. Following example 1,2 what is the difference?

Is a new `pool.spawn` the same as Crystal’s default `spawn` keyword? besides waiting for it to be finish?

```crystal
# Example 1
NestedScheduler::ThreadPool.nursery do |pool|
  pool.spawn { do_same_thing }
end

```

```crystal
# Example 2
NestedScheduler::ThreadPool.nursery do |pool|
  pool.spawn { do_same_thing }
  pool.spawn { do_same_thing }
  # ......
end

```

---

<div class="post-metadata">

**Author:** ![yxhuvud](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/yxhuvud/32/27_2.png) [@yxhuvud](https://forum.crystal-lang.org/u/yxhuvud)\
**Post date:** [January 14, 2024, 2:40pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/9 "2024-01-14T14:40:27Z")

</div>

> [@zw963](#):
>
> Is a new `pool.spawn` the same as Crystal’s default `spawn` keyword? besides waiting for it to be finish?

Yes, except that any fibers that are spawned is run in the pool in question, rather than using the global pool.

---

<div class="post-metadata">

**Author:** ![zw963](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/zw963/32/1623_2.png) [@zw963](https://forum.crystal-lang.org/u/zw963)\
**Post date:** [January 14, 2024, 2:46pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/10 "2024-01-14T14:46:06Z")

</div>

> have made valuable contributions to Crystal in the past.

`In the past` is one of the key point, IMHO

> [@straight-shoota](#):
>
> Especially for complex and integral components such as the scheduler, this is a very difficult task.

I thought there are many much easier and useful parts, create use shards instead of contributing to Crystal itself.

---

<div class="post-metadata">

**Author:** ![zw963](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/zw963/32/1623_2.png) [@zw963](https://forum.crystal-lang.org/u/zw963)\
**Post date:** [January 15, 2024, 7:15am UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/11 "2024-01-15T07:15:37Z")

</div>

> Yes, except that any fibers that are spawned is run in the pool in question, rather than using the global pool.

Hi, thank you for answer, There’s one more question about this, what is the different spawn fiber in two pool instead of only one? (in case only one thread)

---

<div class="post-metadata">

**Author:** ![kojix2](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/kojix2/32/1583_2.png) [@kojix2](https://forum.crystal-lang.org/u/kojix2)\
**Post date:** [August 12, 2025, 6:17am UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/12 "2025-08-12T06:17:34Z")

</div>

Update:

ExecutionContext allows you to use any number of workers.

> **[Fiber::ExecutionContext - Crystal 1.17.1](https://crystal-lang.org/api/1.17.1/Fiber/ExecutionContext.html)**

> <https://github.com/crystal-lang/rfcs/blob/main/text/0002-execution-contexts.md>

Support for WaitGroup is currently in progress.

> <https://github.com/crystal-lang/crystal/issues/15642>
>
> With \`ExecutionContext\` soon being available I think it would be nice to be able… to pass an optional \`ExecutionContext\` to \`WaitGroup\` in which the WG till spawn fibers.
> 
> I'd like to do something like
> \`\`\`crystal
> ec = ExecutionContext::MultiThreaded.new("workers", 4)
> WaitGroup.wait(ec) do |wg|
> items.each { |i| wg.spawn worker(i) }
> end
> \`\`\`
> instead of 
> \`\`\`crystal
> ec = ExecutionContext::MultiThreaded.new("workers", 4)
> ec\_wg = WaitGroup.new(1)
> ec.spawn do
> WaitGroup.wait do |wg|
> items.each { |i| wg.spawn worker(i) }
> end
> ec\_wg.done
> end
> ec\_wg.wait
> \`\`\`
> 
> I guess it would be nice to be able to pass an \`EC\` to the constructor as well.

Thread pools will also become available at some point.

> <https://github.com/crystal-lang/crystal/pull/15885>
>
> \*\*Introduces a pool of threads for execution contexts.\*\*
> 
> This changes the run…time behavior of threads: we no longer start a thread to run a specific scheduler run loop and never terminate in practice (except for the isolated context). Each thread now has its own inner loop that switches to a scheduler loop fiber (the scheduler's main fiber) then switches back to its inner loop (the thread's main fiber) to sleep for a while, then eventually terminates.
> 
> The benefit of the global thread loop is that threads are kept around instead of being created and thrown away. This is for example helpful for #15871 that will allow to move a scheduler to another thread, as well as for applications that regularly start an isolated fiber. They can keep reusing a pending thread instead of having to create one every time.
> 
> Threads still eventually terminate after some configurable inactive time, except for the main thread because we need to keep the main fiber's stack alive.
> 
> A future improvement could park MT threads back into the thread pool, instead of keeping them tied to the MT context. They could be reused by any context that needs parallelism, or to boot a new isolated fiber or ST context, instead of sitting around.
> 
> \*\*NOTE\*\*: the thread pool makes the min/max range in MT contexts kinda moot; it sounds better to settle to a maximum number of schedulers instead.
> 
> \*\*NOTE\*\*: the more we decouple schedulers from threads, the more I'm convinced that using "thread" in the execution context names doesn't make much sense; I'm strongly leaning to rename \`SingleThreaded\` to \`Concurrent\` and \`MultiThreaded\` to \`Parallel\`.
> 
> Extracted from #15871

---

<div class="post-metadata">

**Author:** ![kojix2](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/kojix2/32/1583_2.png) [@kojix2](https://forum.crystal-lang.org/u/kojix2)\
**Post date:** [August 12, 2025, 12:39pm UTC](https://forum.crystal-lang.org/t/how-to-add-command-line-options-to-specify-the-number-of-threads/6286/13 "2025-08-12T12:39:46Z")

</div>

Google NotebookLM is suitable for creating commercials, but if used incorrectly, it can generate a huge amount of garbage content.

[![](https://canada1.discourse-cdn.com/flex036/uploads/crystal_lang/original/2X/1/15236ac6b024219564708199e54f34ad195b6e98.jpeg "Crystal language ExecutionContext RFC0002") ](https://www.youtube.com/watch?v=pzjpEtS8FLQ)
