iOS — Semaphore in Swift

Jackie Leonardy 鄭
4 min readSep 29, 2024

wait()

Hi there! Been a while since the last time I post about iOS and its friends. I was learning about threading someday and found something about Semaphore. Yes, you heard it right, Semaphore as we might know in Computer Science, I’ve never thought of Semaphore being in Swift (Lol I rarely use Multi-Threading or handling concurrency in Swift)

As mentioned in Wikipedia

In computer science, a semaphore is a variable or abstract data type used to control access to a common resource by multiple threads and avoid critical section problems in a concurrent system such as a multitasking operating system.

The keyword here is a synchronization tool that helps manage access to a shared resource by multiple threads.

What I summarize from ChatGPT, a semaphore is like a traffic signal for managing access to a shared resource, such as a parking lot with limited spaces. Imagine there’s a gate with a counter, and the gate only allows a certain number of cars (let’s say 3) into the lot at a time.

Here’s how it works:

  1. Waiting (or “wait” in programming): If there are already 3 cars inside, any new car arriving has to wait until one car leaves. This is like a thread waiting for access to the shared resource.
  2. Signaling (or “signal” in programming): When a car leaves, it signals the gate, allowing one waiting car to enter.

In simple terms, a semaphore controls how many threads (or tasks) can access a resource at the same time. It ensures that not too many tasks try to use the resource simultaneously, preventing problems like data corruption or resource overloading.

In Swift, we can use the DispatchSemaphore class to implement semaphores. It works by maintaining a counter, and threads can signal or wait based on that counter.

How it works:

  • semaphore.wait() decreases the semaphore count. If the count is already 0, the thread will wait until it's signaled.
  • semaphore.signal() increases the count, potentially waking up a waiting thread.

In this example, the semaphore controls access to a critical section of code, making sure that only one task can run at a time. Imagine this real-life situation, an exhibition only allows 2 visitors at the same time, but the visiting time for each users can vary depending on their needs.

import Foundation
import PlaygroundSupport

// Use this in playground to keep the code running
let page = PlaygroundPage.current
page.needsIndefiniteExecution = true

// Only 2 visitors / thread can access at the same time
let entranceStaff: DispatchSemaphore = DispatchSemaphore(value: 2)

DispatchQueue.global().async {
visitorQueueing("Visitor 1")
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: {
visitorFinishesVisiting("Visitor 1")
})
}

DispatchQueue.global().async {
visitorQueueing("Visitor 2")
visitorFinishesVisiting("Visitor 2")
}

DispatchQueue.global().async {
visitorQueueing("Visitor 3")
visitorFinishesVisiting("Visitor 3")
}

func visitorQueueing(_ visitorName: String) {
print("\(visitorName) - Queueing")
entranceStaff.wait()
print("\(visitorName) - can enter now")
}

func visitorFinishesVisiting(_ visitorName: String) {
entranceStaff.signal()
print("\(visitorName) - finish visiting")
}

As we can see every visitor that visit must use wait to tell the entranceStaff that they want to queue, and on the background the staff will check if the current visitor is less than 2 or not, and when the visitor finish visiting the staff will tell (signal()) that one visitor finishes, and allow another visitor to visit.

Why do I need a DispatchQueue?

The semaphore works along with parallel threads meaning that it serves a non-async thread, and if we use Asynchronous Execution for tasks it’s not the use case at all.

In short, using DispatchQueue.global().async introduces concurrency (tasks running in parallel), and the semaphore is needed to control that concurrency.

Use Cases

One common use case is limiting concurrent access to a shared resource like a database or a network request.

let semaphore = DispatchSemaphore(value: 3) // Limit to 3 concurrent tasks
let urls = ["https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg",
"https://example.com/image4.jpg"]

// Simulating a network request to download images
func downloadImage(from url: String, completion: @escaping () -> Void) {
print("Starting download from \(url)")
sleep(2) // Simulate network delay
print("Finished downloading from \(url)")
completion()
}

for url in urls {
DispatchQueue.global().async {
semaphore.wait() // Wait for an available "slot" before starting a task

downloadImage(from: url) {
semaphore.signal() // Release the "slot" after task completes
}
}
}

As shown once the image 1 has finished downloading, it will immediately start downloading image 4 (run task 4)

Tips

  • Never block the main thread with wait() or other blocking calls, as it can cause a deadlock with any UI-related work that also needs to run on the main thread. Always offload long-running or blocking tasks to background threads.
  • DispatchQueue and Memory Management, remember to not cause a strong reference in our task callback, If a task captures a strong reference to an object, and that object also references the closure (directly or indirectly), a strong reference cycle is created, preventing either the task or the object from being released. To avoid this, use weak or unowned references in our closures.
  • DispatchSemaphore itself does not cause memory leaks. It's just an object that controls access to resources. When the tasks calling semaphore.wait() and semaphore.signal() are done executing, the memory they use is released, as long as there are no external strong reference cycles in the closures or objects they reference.
  • When current semaphore slots is 0, using wait() will case a deadlock / freeze in our app, thus a comprehensive unit test using an Injected Semaphore is needed to prevent mis-logic. One of the “workaround” is to use manual counter.

signal()

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (1)

Write a response