Waitgroups in Clojure

Our example demonstrates how to wait for multiple concurrent operations to finish using a CountDownLatch in Clojure.

(ns waitgroups-example.core
  (:require [clojure.core.async :refer [go]]
            [clojure.java.io :as io])
  (:import (java.util.concurrent CountDownLatch)))

;; This is the function we'll run in every thread.
(defn worker [id]
  (println (format "Worker %d starting" id))
  ;; Sleep to simulate an expensive task.
  (Thread/sleep 1000)
  (println (format "Worker %d done" id)))

(defn -main []
  ;; This CountDownLatch is used to wait for all the
  ;; threads launched here to finish.
  (let [latch (CountDownLatch. 5)]
    ;; Launch several threads and decrement the CountDownLatch
    ;; counter for each.
    (dotimes [i 5]
      ;; Wrap the worker call in a future that makes sure to tell
      ;; the CountDownLatch that this worker is done. This way the worker
      ;; itself does not have to be aware of the concurrency primitives
      ;; involved in its execution.
      (go
        (try
          (worker (inc i))
          (finally
            (.countDown latch)))))

    ;; Block until the CountDownLatch counter goes back to 0;
    ;; all the workers notified they're done.
    (.await latch)

    ;; Note that this approach has no straightforward way
    ;; to propagate errors from workers. For more
    ;; advanced use cases, consider using core.async's error handling capabilities.
    ))

(comment
  (-main)
  )

To run the program:

$ lein run
Worker 1 starting
Worker 5 starting
Worker 4 starting
Worker 3 starting
Worker 2 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done

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

In this Clojure version:

  1. We use clojure.core.async/go to create lightweight threads (similar to goroutines).
  2. Instead of a WaitGroup, we use Java’s CountDownLatch, which serves a similar purpose.
  3. We use dotimes to launch multiple workers, each in its own go block.
  4. The latch is countdown in a finally block to ensure it happens even if an exception occurs.
  5. We use .await on the latch to wait for all workers to complete, similar to WaitGroup.Wait().

This example demonstrates concurrent programming in Clojure, showing how to manage multiple concurrent operations and wait for their completion.