lib/async/semaphore.rb
# frozen_string_literal: true # Released under the MIT License. # Copyright, 2018-2023, by Samuel Williams. require_relative 'list' module Async # A synchronization primitive, which limits access to a given resource. # @public Since `stable-v1`. class Semaphore # @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks. # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks. def initialize(limit = 1, parent: nil) @count = 0 @limit = limit @waiting = List.new @parent = parent end # The current number of tasks that have acquired the semaphore. attr :count # The maximum number of tasks that can acquire the semaphore. attr :limit # The tasks waiting on this semaphore. attr :waiting # Allow setting the limit. This is useful for cases where the semaphore is used to limit the number of concurrent tasks, but the number of tasks is not known in advance or needs to be modified. # # On increasing the limit, some tasks may be immediately resumed. On decreasing the limit, some tasks may execute until the count is < than the limit. # # @parameter limit [Integer] The new limit. def limit= limit difference = limit - @limit @limit = limit # We can't suspend if difference > 0 difference.times do break unless node = @waiting.first node.resume end end end # Is the semaphore currently acquired? def empty? @count.zero? end # Whether trying to acquire this semaphore would block. def blocking? @count >= @limit end # Run an async task. Will wait until the semaphore is ready until spawning and running the task. def async(*arguments, parent: (@parent or Task.current), **options) wait parent.async(**options) do |task| @count += 1 begin yield task, *arguments ensure self.release end end end # Acquire the semaphore, block if we are at the limit. # If no block is provided, you must call release manually. # @yields {...} When the semaphore can be acquired. # @returns The result of the block if invoked. def acquire wait @count += 1 return unless block_given? begin return yield ensure self.release end end # Release the semaphore. Must match up with a corresponding call to `acquire`. Will release waiting fibers in FIFO order. def release @count -= 1 while (@limit - @count) > 0 and node = @waiting.first node.resume end end private class FiberNode < List::Node def initialize(fiber) @fiber = fiber end def resume if @fiber.alive? Fiber.scheduler.resume(@fiber) end end end private_constant :FiberNode # Wait until the semaphore becomes available. def wait return unless blocking? @waiting.stack(FiberNode.new(Fiber.current)) do Fiber.scheduler.transfer while blocking? end end end end