Waitgroups in Crystal

Our example demonstrates how to wait for multiple fibers to finish using a Channel. In Crystal, fibers are lightweight concurrent units of execution, similar to goroutines in other languages.

require "channel"

# This is the function we'll run in every fiber.
def worker(id : Int32)
  puts "Worker #{id} starting"
  # Sleep to simulate an expensive task.
  sleep 1
  puts "Worker #{id} done"
end

# This Channel is used to wait for all the fibers launched here to finish.
done_channel = Channel(Nil).new

# Launch several fibers and send a signal to the channel when each is done.
5.times do |i|
  spawn do
    worker(i + 1)
    done_channel.send(nil)
  end
end

# Wait for all fibers to finish by receiving from the channel once for each fiber.
5.times { done_channel.receive }

# Note that this approach has no straightforward way to propagate errors from workers.
# For more advanced use cases, consider using a more robust error handling mechanism.

To run the program:

$ crystal run waitgroups.cr
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 Crystal version:

  1. We use a Channel to synchronize the fibers, which is similar to using a WaitGroup in other languages.

  2. The spawn keyword is used to create new fibers, which are Crystal’s lightweight threads.

  3. Instead of explicitly incrementing and decrementing a counter, we send a signal to the channel when each fiber is done.

  4. We wait for all fibers to finish by receiving from the channel once for each fiber we spawned.

  5. Crystal’s sleep function is used to simulate work, similar to time.Sleep in the original example.

This approach achieves the same goal of waiting for multiple concurrent operations to complete before proceeding. However, as noted, it doesn’t provide a built-in way to propagate errors from the workers. For more complex scenarios, you might need to implement additional error handling mechanisms.