Skip to content

Advanced GCD Concepts

Dispatch Work Item Flags

DispatchWorkItemFlags are options that modify the behavior of a DispatchWorkItem when dispatched to a queue in Grand Central Dispatch (GCD). These flags allow fine-grained control over task execution, such as enforcing serial execution, inheriting quality-of-service (QoS), or preventing execution altogether. They are passed during the creation of a DispatchWorkItem to customize its scheduling and runtime behavior.

Available Flags:

  • .barrier: Ensures the work item acts as a barrier, executing only after all prior tasks on the queue complete, and preventing subsequent tasks from starting until it finishes. Useful for synchronizing access to shared resources on concurrent queues.
  • .detached: Executes the work item independently of the queue’s QoS, using a default or explicitly specified QoS. This detaches the task from the queue’s context.
  • .assignCurrentContext: Inherits the QoS and attributes of the queue or thread from which the work item is dispatched, ensuring consistency with the caller’s context.
  • .noQoS: Ignores QoS settings, using a default priority instead. Rarely used, as QoS typically optimizes performance.
  • .enforceQoS: Forces the work item to use its explicitly set QoS, overriding any inherited QoS from the queue or context.
  • .inheritQoS: Explicitly inherits the QoS of the dispatching context (similar to .assignCurrentContext but more explicit).

Example: Using .barrier flag to synchronize access:

swift
let workItem = DispatchWorkItem(flags: .barrier) {
    print("Barrier work item executing on \(Thread.current)")
    // Update shared resource
}
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async(execute: workItem)

Use Case: Flags are used when precise control over task execution is needed, such as ensuring exclusive access to a resource or aligning QoS with specific requirements.

Dispatch Barrier

A Dispatch Barrier is a GCD feature that ensures a task (work item or block) executes exclusively on a concurrent queue, acting as a synchronization point. When a barrier task is dispatched, it waits for all previously submitted tasks on the queue to complete before executing, and no subsequent tasks start until the barrier task finishes. This is critical for safely updating shared resources on concurrent queues without race conditions.

Key Characteristics:

  • Only effective on concurrent queues created with .concurrent attribute. On serial queues, barriers behave like regular tasks since tasks already execute sequentially.
  • Ensures thread-safe access to shared resources, such as a database or in-memory cache.
  • Often used with the .barrier flag in DispatchWorkItem or via asyncAfter with barrier-specific methods.

Use Case: Updating a shared array or dictionary on a concurrent queue, ensuring no other tasks read or write during the update.

Dispatch Barrier Implementation

Example: Managing a shared cache with barriers:

swift
class CacheManager {
    private let queue = DispatchQueue(label: "com.example.cache", attributes: .concurrent)
    private var cache: [String: String] = [:]

    func read(key: String) -> String? {
        var result: String?
        queue.sync {
            result = cache[key]
            print("Read \(key): \(result ?? "nil") on \(Thread.current)")
        }
        return result
    }

    func write(key: String, value: String) {
        queue.async(flags: .barrier) {
            self.cache[key] = value
            print("Wrote \(key): \(value) on \(Thread.current)")
        }
    }
}

let cacheManager = CacheManager()

// Simulate concurrent reads and writes
DispatchQueue.global().async {
    cacheManager.write(key: "user", value: "Alice")
}
DispatchQueue.global().async {
    print("Read result: \(cacheManager.read(key: "user") ?? "nil")")
}
DispatchQueue.global().async {
    cacheManager.write(key: "user", value: "Bob")
}
DispatchQueue.global().async {
    print("Read result: \(cacheManager.read(key: "user") ?? "nil")")
}

Explanation:

  • The queue is concurrent, allowing multiple reads to occur simultaneously.
  • write uses a barrier to ensure exclusive access during updates, preventing race conditions.
  • read uses sync to safely access the cache without modifying it.
  • Output (order may vary, but writes are atomic):
    Read result: nil
    Wrote user: Alice on <Thread 0x...>
    Read result: Alice
    Wrote user: Bob on <Thread 0x...>
    Read result: Bob

Use Case Example: Updating a shared image processing buffer in a photo-editing app, ensuring no reads occur during buffer modifications.

Dispatch Semaphore

A Dispatch Semaphore is a GCD synchronization primitive that controls access to a shared resource by maintaining a count of available "permits." Tasks must wait for a permit (via wait) before proceeding and release it (via signal) when done. Semaphores are used to limit concurrent access to resources, prevent overuse, or enforce mutual exclusion.

Key Characteristics:

  • Initialized with a non-negative count (e.g., DispatchSemaphore(value: 2) allows two concurrent tasks).
  • wait() decrements the count; if the count is zero, the thread blocks until a permit is available.
  • signal() increments the count, potentially unblocking a waiting task.
  • Useful for resource pools (e.g., limiting network connections) or critical sections.

Use Case: Limiting the number of concurrent network requests to avoid overwhelming a server.

Critical Section

A Critical Section is a portion of code that accesses a shared resource (e.g., a variable, file, or database) and must be executed by only one thread at a time to prevent race conditions. In GCD, critical sections are typically protected using mechanisms like dispatch barriers, semaphores, or serial queues to ensure mutual exclusion.

Example Problem Without Critical Section:

swift
var counter = 0
let queue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
queue.async { for _ in 0..<1000 { counter += 1 } }
queue.async { for _ in 0..<1000 { counter += 1 } }
// counter may be less than 2000 due to race conditions

Solution: Use a semaphore or barrier to protect the critical section (see implementation below).

Use Case: Updating a shared database, modifying a file, or managing a connection pool.

Priority Inversion

Priority Inversion occurs when a low-priority task holds a resource needed by a high-priority task, causing the high-priority task to wait and effectively run at the lower priority. This can degrade performance or cause delays in time-sensitive operations, such as UI rendering in iOS.

Example Scenario:

  • A low-priority background task (e.g., file I/O) holds a semaphore.
  • A high-priority task (e.g., UI update) waits for the semaphore.
  • The low-priority task is preempted by medium-priority tasks, delaying the high-priority task.

Mitigation in GCD:

  • Use appropriate QoS levels (e.g., .userInteractive for UI tasks) to prioritize tasks.
  • Avoid long-running tasks in critical sections.
  • Use DispatchQueue with .concurrent and barriers instead of semaphores where possible, as GCD optimizes scheduling.
  • iOS may apply priority inheritance to boost the priority of tasks holding resources, but explicit QoS management is recommended.

Use Case: Ensuring a high-priority UI task isn’t delayed by a background data sync holding a shared resource.

Dispatch Semaphore Implementation

Example 1: Protecting a Critical Section with a Semaphore:

swift
class Counter {
    private var count = 0
    private let semaphore = DispatchSemaphore(value: 1) // Mutual exclusion
    private let queue = DispatchQueue(label: "com.example.counter", attributes: .concurrent)

    func increment() {
        semaphore.wait() // Acquire permit
        defer { semaphore.signal() } // Release permit
        count += 1
        print("Incremented to \(count) on \(Thread.current)")
    }

    func getCount() -> Int {
        semaphore.wait()
        defer { semaphore.signal() }
        return count
    }
}

let counter = Counter()
for _ in 0..<5 {
    counter.queue.async {
        counter.increment()
    }
}

DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
    print("Final count: \(counter.getCount())")
}

Explanation:

  • The semaphore with value: 1 ensures only one thread can access count at a time, creating a critical section.
  • defer { semaphore.signal() } guarantees the semaphore is released, even if an error occurs.
  • Output (order varies, but count is correct):
    Incremented to 1 on <Thread 0x...>
    Incremented to 2 on <Thread 0x...>
    Incremented to 3 on <Thread 0x...>
    Incremented to 4 on <Thread 0x...>
    Incremented to 5 on <Thread 0x...>
    Final count: 5

Example 2: Limiting Concurrent Network Requests:

swift
class NetworkManager {
    private let semaphore = DispatchSemaphore(value: 3) // Allow 3 concurrent requests
    private let queue = DispatchQueue(label: "com.example.network", attributes: .concurrent)

    func fetchData(url: String, completion: @escaping (String) -> Void) {
        queue.async {
            self.semaphore.wait()
            defer { self.semaphore.signal() }
            
            print("Fetching \(url) on \(Thread.current)")
            Thread.sleep(forTimeInterval: Double.random(in: 1...3)) // Simulate network
            completion("Data from \(url)")
        }
    }
}

let networkManager = NetworkManager()
let urls = ["url1", "url2", "url3", "url4", "url5"]
let group = DispatchGroup()

for url in urls {
    group.enter()
    networkManager.fetchData(url: url) { result in
        print(result)
        group.leave()
    }
}

group.notify(queue: .main) {
    print("All requests complete")
}

Explanation:

  • The semaphore limits concurrency to three requests at a time.
  • wait() blocks additional tasks if three are active, and signal() frees a permit when a task completes.
  • Output (order varies, max three tasks run concurrently):
    Fetching url1 on <Thread 0x...>
    Fetching url2 on <Thread 0x...>
    Fetching url3 on <Thread 0x...>
    Data from url2
    Fetching url4 on <Thread 0x...>
    Data from url1
    Fetching url5 on <Thread 0x...>
    Data from url3
    Data from url4
    Data from url5
    All requests complete

Dispatch Sources

A Dispatch Source is a GCD mechanism that monitors system events or conditions and triggers a handler when those events occur. Dispatch Sources are used to handle asynchronous events like timers, file system changes, network activity, or process signals. They integrate with GCD queues, allowing event handlers to execute on specified queues, ensuring thread safety and efficient resource management.

Key Characteristics:

  • Types of Dispatch Sources: Include timers, file descriptors, process events, signals, and custom events.
  • Event Handling: A dispatch source triggers a user-defined closure when the monitored event occurs (e.g., a timer firing or data arriving on a socket).
  • State Management: Sources can be suspended, resumed, or canceled, and they support merging of events to reduce handler invocations.
  • Thread Safety: Handlers execute on the specified queue, ensuring safe integration with other GCD tasks.

Common Dispatch Source Types:

  • Timer: Fires at regular intervals or a specific time (e.g., scheduling periodic tasks).
  • Data: Monitors custom data events (e.g., accumulating data changes).
  • File System: Tracks file or directory changes (e.g., file writes or deletions).
  • Signal: Handles UNIX signals (e.g., SIGTERM).
  • Socket: Monitors network socket activity (e.g., incoming data).
  • Process: Tracks process state changes (e.g., process termination).

Use Case: Creating a timer to periodically check for updates, monitoring file changes in a directory, or handling incoming network data.

Dispatch Source Implementation

Below are examples demonstrating the use of dispatch sources, focusing on a timer and a custom data source.

Example 1: Creating a Repeating Timer with Dispatch Source:

swift
class TimerManager {
    private var timer: DispatchSourceTimer?
    private let queue = DispatchQueue(label: "com.example.timer")

    func startTimer(interval: Double, handler: @escaping () -> Void) {
        timer = DispatchSource.makeTimerSource(queue: queue)
        timer?.schedule(deadline: .now(), repeating: interval)
        timer?.setEventHandler {
            print("Timer fired at \(Date()) on \(Thread.current)")
            handler()
        }
        timer?.resume()
    }

    func stopTimer() {
        timer?.cancel()
        timer = nil
        print("Timer cancelled")
    }
}

let timerManager = TimerManager()
timerManager.startTimer(interval: 1.0) {
    print("Performing periodic task")
}

// Stop timer after 5 seconds
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
    timerManager.stopTimer()
}

Explanation:

  • A DispatchSourceTimer is created on a custom queue.
  • schedule(deadline:repeating:) sets the timer to fire every 1 second, starting immediately.
  • The setEventHandler closure runs on the specified queue (queue) each time the timer fires.
  • resume() starts the timer, and cancel() stops it.
  • Output (approximate):
    Timer fired at 2025-06-15 01:45:01 +0000 on <Thread 0x...>
    Performing periodic task
    Timer fired at 2025-06-15 01:45:02 +0000 on <Thread 0x...>
    Performing periodic task
    ...
    Timer fired at 2025-06-15 01:45:05 +0000 on <Thread 0x...>
    Performing periodic task
    Timer cancelled
  • Use Case: Periodic tasks like polling a server for updates or refreshing a UI element.

Example 2: Custom Data Source for Accumulating Events:

swift
class DataMonitor {
    private let source: DispatchSourceUserDataAdd
    private let queue = DispatchQueue(label: "com.example.data")
    private var counter: UInt = 0

    init() {
        source = DispatchSource.makeUserDataAddSource(queue: queue)
        source.setEventHandler { [weak self] in
            guard let self = self else { return }
            let data = self.source.data
            print("Received data: \(data) on \(Thread.current)")
            self.counter += data
            print("Total count: \(self.counter)")
        }
        source.resume()
    }

    func addData(_ value: UInt) {
        source.add(data: value)
    }

    func cancel() {
        source.cancel()
        print("Data source cancelled")
    }
}

let monitor = DataMonitor()

// Simulate adding data from multiple threads
DispatchQueue.global().async {
    for i in 1...3 {
        monitor.addData(UInt(i))
        Thread.sleep(forTimeInterval: 0.5)
    }
}

DispatchQueue.global().async {
    for i in 4...6 {
        monitor.addData(UInt(i))
        Thread.sleep(forTimeInterval: 0.7)
    }
}

// Cancel after 5 seconds
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
    monitor.cancel()
}

Explanation:

  • A DispatchSourceUserDataAdd source is created to accumulate custom data events.
  • add(data:) adds values to the source, which are merged and delivered to the setEventHandler closure.
  • The handler processes the accumulated data (sum of added values since last invocation) and updates a counter.
  • resume() starts the source, and cancel() stops it.
  • Output (order varies, data may be merged):
    Received data: 1 on <Thread 0x...>
    Total count: 1
    Received data: 4 on <Thread 0x...>
    Total count: 5
    Received data: 2 on <Thread 0x...>
    Total count: 7
    Received data: 5 on <Thread 0x...>
    Total count: 12
    Received data: 3 on <Thread 0x...>
    Total count: 15
    Data source cancelled
  • Use Case: Aggregating events from multiple sources, such as user interactions or sensor data, before processing.

Example 3: Combining Dispatch Source with Dispatch Group:

swift
class FileMonitor {
    private let source: DispatchSourceFileSystemObject
    private let queue = DispatchQueue(label: "com.example.file")
    private let group = DispatchGroup()

    init(filePath: String) {
        let descriptor = open(filePath, O_EVTONLY) // Open file for monitoring
        guard descriptor != -1 else {
            fatalError("Failed to open file")
        }
        
        source = DispatchSource.makeFileSystemObjectSource(
            fileDescriptor: descriptor,
            eventMask: .write,
            queue: queue
        )
        
        source.setEventHandler { [weak self] in
            print("File modified at \(Date()) on \(Thread.current)")
            self?.group.leave()
        }
        
        source.setCancelHandler {
            close(descriptor)
            print("File monitoring cancelled")
        }
        
        group.enter()
        source.resume()
    }

    func waitForModification(timeout: DispatchTime) {
        let result = group.wait(timeout: timeout)
        print("Wait result: \(result)")
    }

    func cancel() {
        source.cancel()
    }
}

let filePath = "/tmp/test.txt"
let monitor = FileMonitor(filePath: filePath)

// Simulate file modification
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    let handle = FileHandle(forWritingAtPath: filePath)
    handle?.write("Test data".data(using: .utf8)!)
    handle?.closeFile()
}

// Wait for modification or timeout
monitor.waitForModification(timeout: .now() + 3)

// Cancel monitoring
DispatchQueue.global().asyncAfter(deadline: .now() + 4) {
    monitor.cancel()
}

Explanation:

  • A DispatchSourceFileSystemObject monitors a file for write events.
  • The source is tied to a DispatchGroup to signal completion when a modification occurs.
  • setEventHandler runs when the file is modified, and setCancelHandler cleans up resources.
  • wait(timeout:) blocks until the file is modified or the timeout occurs.
  • Output (assuming file exists):
    File modified at 2025-06-15 01:45:02 +0000 on <Thread 0x...>
    Wait result: .success
    File monitoring cancelled
  • Use Case: Watching a log file for changes in a debugging tool or monitoring a configuration file for updates.

Released under the MIT License.