Worker Pools in Scheme

(import (chezscheme))

;; 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.
(define (worker id jobs results)
  (let loop ()
    (when (channel-ready? jobs)
      (let ((j (channel-get jobs)))
        (display (format "worker ~a started  job ~a\n" id j))
        (sleep 1)
        (display (format "worker ~a finished job ~a\n" id j))
        (channel-put results (* j 2))
        (loop)))))

(define (main)
  ;; In order to use our pool of workers we need to send
  ;; them work and collect their results. We make 2
  ;; channels for this.
  (define num-jobs 5)
  (define jobs (make-channel num-jobs))
  (define results (make-channel num-jobs))

  ;; This starts up 3 workers, initially blocked
  ;; because there are no jobs yet.
  (do ((w 1 (+ w 1)))
      ((> w 3))
    (fork-thread (lambda () (worker w jobs results))))

  ;; Here we send 5 `jobs` and then `close` that
  ;; channel to indicate that's all the work we have.
  (do ((j 1 (+ j 1)))
      ((> j num-jobs))
    (channel-put jobs j))
  (channel-close jobs)

  ;; Finally we collect all the results of the work.
  ;; This also ensures that the worker threads have
  ;; finished. An alternative way to wait for multiple
  ;; threads is to use a semaphore.
  (do ((a 1 (+ a 1)))
      ((> a num-jobs))
    (channel-get results)))

(main)

This Scheme implementation uses Chez Scheme’s built-in concurrency primitives. Here’s an explanation of the key differences and adaptations:

  1. We use fork-thread to create new threads, which is similar to goroutines in Go.
  2. Instead of Go’s channels, we use Chez Scheme’s make-channel, channel-put, channel-get, and channel-ready? functions.
  3. The worker function is implemented as a recursive loop that continues as long as there are jobs available.
  4. We use display and format for output instead of Go’s fmt.Println.
  5. The sleep function is used to simulate work, similar to Go’s time.Sleep.
  6. Instead of Go’s range loops, we use Scheme’s do loops for iteration.
  7. Channel closing is done with channel-close.

To run this program, you would save it to a file (e.g., worker-pools.scm) and run it with a Chez Scheme interpreter:

$ chez --script worker-pools.scm
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

Note that the exact order of output may vary due to the concurrent nature of the program. The program demonstrates how to implement a worker pool pattern in Scheme, using threads and channels to distribute work and collect results.