Home > OS >  Swift equivalent of Ruby’s Concurrent::Event?
Swift equivalent of Ruby’s Concurrent::Event?

Time:09-21

The popular Concurrent-Ruby library has a Concurrent::Event class that I find wonderful. It very neatly encapsulates the idea of, “Some threads need to wait for another thread to finish something before proceeding.”

It only takes three lines of code to use:

  • One to create the object
  • One to call .wait to start waiting, and
  • One to call .set when the thing is ready.

All the locks and booleans you’d need to use to create this out of other concurrency primitives are taken care of for you.

To quote some of the documentation, along with with a sample usage:

Old school kernel-style event reminiscent of Win32 programming in C .

When an Event is created it is in the unset state. Threads can choose to #wait on the event, blocking until released by another thread. When one thread wants to alert all blocking threads it calls the #set method which will then wake up all listeners. Once an Event has been set it remains set. New threads calling #wait will return immediately.

require 'concurrent-ruby'

event = Concurrent::Event.new

t1 = Thread.new do
  puts "t1 is waiting"
  event.wait
  puts "event ocurred"
end

t2 = Thread.new do
  puts "t2 calling set"
  event.set
end

[t1, t2].each(&:join)

which prints output like the following

t1 is waiting
t2 calling set
event occurred

(Several different orders are possible because it is multithreaded, but ‘t2 calling set’ always comes out before ‘event occurred’.)


Is there something like this in Swift on iOS?

CodePudding user response:

I think the closest thing to that is the new async/await syntax in Swift 5.5. There's no equivalent of event.set, but await waits for something asynchronous to finish. A particularly nice expression of concurrency is async let, which proceeds concurrently but then lets you pause to gather up all the results of the async let calls:

async let result1 = // do something asynchronous
async let result2 = // do something else asynchronous at the same time
// ... and keep going...
// now let's gather up the results
return await (result1, result2)

CodePudding user response:

You can achieve the result in your example using a Grand Central Dispatch DispatchSemaphore - This is a traditional counting semaphore. Each call to signal increments the semaphore. Each call to wait decrement the semaphore and if the result is less than zero it blocks and waits until the semaphore is 0

let semaphore = DispatchSemaphore(value: 0)

let q1 = DispatchQueue(label:"q1", target: .global(qos: .utility))

let q2 = DispatchQueue(label:"q2", target: .global(qos: .utility))

q1.async {
   print("q1 is waiting")
   semaphore.wait()
   print("event occurred")
}

q2.async {
   print("q2 calling signal")
   semaphore.signal()
}

Output:

q1 is waiting

q2 calling signal

event occurred

But this object won't work if you have multiple threads that want to wait. Since each call to wait decrements the semaphore the other tasks would remain blocked.

For that you could use a DispatchGroup. You call enter before you start a task in the group and leave when it is done. You can use wait to block until the group is empty, and like your Ruby object, wait will not block if the group is already empty and multiple threads can wait on the same group.

let group = DispatchGroup()

let q1 = DispatchQueue(label:"q1", target: .global(qos: .utility))

let q2 = DispatchQueue(label:"q2", target: .global(qos: .utility))

q1.async {
    print("q1 is waiting")
    group.wait()
        print("event occurred")
}

group.enter()
q2.async {
    print("q2 calling leave")
    group.leave()
}

Output:

q1 is waiting

q2 calling leave

event occurred

You generally want to avoid blocking threads on iOS if possible as there is a risk of deadlocks and if you block the main thread your whole app will become non responsive. It is more common to use notify to schedule code to execute when the group becomes empty.

I understand that your code is simply a contrived example, but depending on what you actually want to do and your minimum supported iOS requirements, there may be better alternatives.

  • DispatchGroup to execute code when several asynchronous tasks are complete using notify rather than wait
  • Combine to process asynchronous events in a pipeline (iOS 13 )
  • Async/Await (iOS 15 )
  • Related