0
votes

I implemented a semaphore today and it raised some questions about semaphores, threads, and queues. Are my following notations accurate?

let semaphore = DispatchSemaphore(value: 1)
let serialQueue = DispatchQueue(label: "serial")

someAsyncMethod {

    serialQueue.async {

        // When someAsyncMethod returns, this queue, behind the scenes,
        // creates a Thread/NSThread object and approaches the
        // semaphore and it is this thread object that decrements
        // the semaphore counter.

        // The next time someAsyncMethod returns, a new thread object
        // is created behind the scenes (by the queue) and this thread
        // object is made to wait until something (anything) signals
        // it is done.

        semaphore.wait()

        // do work...
        someMethod()

    }

}

func someMethod() {

    // do work...

    // This task may ultimately be on a different thread than
    // where it started despite being in the same queue
    // (since not every task is guaranteed to run on the same thread)
    // but it doesn't matter, any object can signal the semaphore.

    semaphore.signal()

}
  1. Do semaphores respond to specific thread objects/instances?
  2. Is a new thread object/instance created every time someAsyncMethod returns and enters the queue?
3

3 Answers

0
votes

Semaphores are not thread-specific. The whole point is to coordinate among threads, so they have to be usable by multiple threads.

In this specific case, there's no need for a semaphore (if those are the only uses) because you're using a serial queue. By definition, a serial queue only allows one of the tasks queued to it to run at a time, in first-in-first-out order. That is, the second task doesn't need to wait on the semaphore to avoid running simultaneously with the first task because it isn't even allowed to start until the first task has completed.

When you put a task on a queue asynchronously, the calling code can continue on immediately. The task is more like a data object being put into a data structure (queue). Grand Central Dispatch uses a pool of threads to pop a task off of a queue and execute it. Whether it has to create a new thread or not depends on whether there are sufficient idle threads in the pool already.

0
votes

The basics are this:

A queue is a tool for managing a collection of tasks.

Every task is executed on a thread.

A semaphore is way of coordinating tasks, which might be running on different threads.

Whether each task creates a new thread, or only some tasks do, or maybe all tasks run on the same thread is entirely up to Grand Central Dispatch. GCD relieves you of the burden of worrying about threads. Threads will be created, destroyed, or reused as GCD sees fit according to the capabilities, resources, and load of your system.

Which is just a polite way of saying "stop thinking about threads." Assume each task will run on its own thread in its own time (with the exception of very special queues like Main). Whether the thread that a task actually executes on is a new thread or a reused thread, and whether that thread gets destroyed after the task runs or gets reused for another task is the domain of the man behind the curtain.

If you need to coordinate a pool of resources or wait for a set of tasks to complete, that's when you use semaphores and/or any of the queue-based tools like serial queues, dispatch groups, barriers, and so on.

I hope that helps.

-1
votes

Let’s tackle these one at a time:

someAsyncMethod {

    serialQueue.async {

        // When someAsyncMethod returns, this queue, behind the scenes,
        // creates a Thread/NSThread object and approaches the
        // semaphore and it is this thread object that decrements
        // the semaphore counter.

        // The next time someAsyncMethod returns, a new thread object
        // is created behind the scenes (by the queue) and this thread
        // object is made to wait until something (anything) signals
        // it is done.

        semaphore.wait()

        // do work...
        someMethod()
    }
}

First, a minor detail, but async doesn’t “create” threads. GCD has a pool of worker threads, and async just grabs one of these idle worker threads. (It’s why GCD is performant, as it avoids creating/destroying threads all the time, which is a costly exercise. It draws upon its pool of worker threads.)

The second time you call async on the same serial queue, you might get the same thread the next time. Or you might get a different worker thread from the pool. You have no assurances either way. Your only assurance is that the queue is serial (because you defined it as such).

But, yes, if the semaphore started with a count of 1, the first time it would just decrement the counter and carry on. And, yes, if that semaphore hadn’t yet been signaled by the time you got to that second wait, yes it would wait for a signal.

But the idea of using this non-zero dispatch semaphore in conjunction with a serial queue seems highly suspect. Usually you use serial queues to coordinate different tasks, or, in rare cases, use semaphores, but almost never both at the same time. Generally, the presence of semaphores, at all, is concerning, because there are almost always better solutions available.

You then had:

func someMethod() {

    // do work...

    // This task may ultimately be on a different thread than
    // where it started despite being in the same queue
    // (since not every task is guaranteed to run on the same thread)
    // but it doesn't matter, any object can signal the semaphore.

    semaphore.signal()

}

This code, because it was called from inside the previous code block’s serialQueue.async, will absolutely still be running on the same thread from which you called someMethod.

So, therefore, this code doesn’t quite make sense. You would never signal from the same thread that would call wait on the same thread. The whole point of semaphores is that one thread can wait for a signal from a different thread.

E.g. it might make sense if, for example, someMethod was doing something equivalent to:

func someMethod() {
    someOtherQueue.async {
        // do work...

        // Because this is dispatched to a different queue and we know is
        // now running on a different thread, now the semaphore has some 
        // utility, because it only works if it’s a different thread than
        // where we’ll later call `wait`...

        semaphore.signal()
    }
}

Now, all of this begs the question about the purpose of the serial queue in conjunction with this semaphore. I’m afraid that in the attempt to abstract this away from the unrelated details of your code base, you’ve made this a bit too abstract. So it’s hard to advise you on a better pattern without understanding the broader problem this code was attempting to solve.

But, with no offense intended, there almost certainly are going to be much better approaches. We can’t just can’t advise on the basis of the information provided thus far.


You concluded with two questions:

  1. Do semaphores respond to specific thread objects/instances?

Nope. It’s for one thread to signal to another, different, thread. But there are no other constraints other than that.

  1. Is a new thread object/instance created every time someAsyncMethod returns and enters the queue?

As I hope I’ve made clear above, the closure dispatched to serial queue with async may end up on the same worker thread or may end up on another worker thread.

But that’s not the real concern here. The real concern is that the wait and signal calls are made from separate threads, not whether subsequent wait calls are.