Waitgroups in Racket

Our first example demonstrates how to simulate concurrent execution in Racket using threads. While Racket doesn’t have an exact equivalent to Go’s WaitGroups, we can achieve similar functionality using semaphores.

#lang racket

(require racket/place)

;; This is the function we'll run in every thread.
(define (worker id)
  (printf "Worker ~a starting\n" id)
  ;; Sleep to simulate an expensive task.
  (sleep 1)
  (printf "Worker ~a done\n" id))

(define (main)
  ;; This semaphore is used to wait for all the
  ;; threads launched here to finish.
  (define sem (make_semaphore 0))

  ;; Launch several threads and increment the semaphore
  ;; counter for each.
  (for ([i (in-range 1 6)])
    (thread
     (lambda ()
       ;; Wrap the worker call in a lambda that makes sure to signal
       ;; the semaphore that this worker is done. This way the worker
       ;; itself does not have to be aware of the concurrency primitives
       ;; involved in its execution.
       (dynamic-wind
           void
           (lambda () (worker i))
           (lambda () (semaphore-post sem))))))

  ;; Block until the semaphore counter goes back to 0;
  ;; all the workers notified they're done.
  (for ([_ (in-range 5)])
    (semaphore-wait sem))

  ;; Note that this approach has no straightforward way
  ;; to propagate errors from workers. For more
  ;; advanced use cases, consider using Racket's
  ;; custodians or other error handling mechanisms.
)

(main)

To run the program:

$ racket waitgroups.rkt
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done

The order of workers starting up and finishing is likely to be different for each invocation.

In this Racket version:

  1. We use thread to create separate threads, which are similar to goroutines in Go.
  2. Instead of a WaitGroup, we use a semaphore to coordinate the threads.
  3. We use semaphore-post to signal that a thread is done, which is equivalent to wg.Done() in Go.
  4. We use semaphore-wait in a loop to wait for all threads to finish, which is similar to wg.Wait() in Go.
  5. The dynamic-wind function is used to ensure that the semaphore is signaled even if an error occurs in the worker function.

While this approach simulates the behavior of Go’s WaitGroups, it’s worth noting that Racket has its own idiomatic ways of handling concurrency, such as places, futures, and engines, which might be more appropriate depending on the specific use case.