Worker Pools in Nim

Our example demonstrates how to implement a worker pool using threads and channels in Nim.

import std/[os, strformat]

# Here's the worker, of which we'll run several
# concurrent instances. These workers will receive
# work on the `jobs` channel and send the corresponding
# results on `results`. We'll sleep a second per job to
# simulate an expensive task.
proc worker(id: int, jobs, results: ptr Channel[int]) {.thread.} =
  while true:
    let job = jobs[].recv()
    if job == 0:  # Use 0 as a sentinel value to stop the worker
      break
    echo fmt"worker {id} started  job {job}"
    sleep(1000)
    echo fmt"worker {id} finished job {job}"
    results[].send(job * 2)

proc main() =
  const numJobs = 5
  var
    jobs = newChannel[int](numJobs)
    results = newChannel[int](numJobs)
    threads: array[3, Thread[tuple[id: int, jobs, results: ptr Channel[int]]]]

  # This starts up 3 workers, initially blocked
  # because there are no jobs yet.
  for i in 0..2:
    createThread(threads[i], worker, (i+1, addr jobs, addr results))

  # Here we send 5 `jobs` and then send sentinel values
  # to indicate that's all the work we have.
  for j in 1..numJobs:
    jobs.send(j)
  
  for _ in 0..2:
    jobs.send(0)  # Send sentinel value to stop workers

  # Finally we collect all the results of the work.
  # This also ensures that the worker threads have
  # finished.
  for _ in 1..numJobs:
    discard results.recv()

  # Wait for all threads to finish
  joinThreads(threads)

main()

Our program shows the 5 jobs being executed by various workers. The program only takes about 2 seconds despite doing about 5 seconds of total work because there are 3 workers operating concurrently.

To run the program, save it as worker_pools.nim and use the Nim compiler:

$ nim c -r worker_pools.nim
worker 1 started  job 1
worker 2 started  job 2
worker 3 started  job 3
worker 1 finished job 1
worker 1 started  job 4
worker 2 finished job 2
worker 2 started  job 5
worker 3 finished job 3
worker 1 finished job 4
worker 2 finished job 5

The execution time should be around 2 seconds, despite the program doing about 5 seconds of total work. This is because the three worker threads are operating concurrently.

Note that Nim’s concurrency model is different from Go’s. In this example, we use Nim’s system threads and channels to create a similar worker pool pattern. The Thread type and createThread procedure are used to spawn OS threads, while Channel is used for inter-thread communication.