Sobes.tech
Middle

How to synchronize data between threads (context) in iOS?

sobes.tech AI

Answer from AI

For synchronization, the following mechanisms can be used:

  1. NSLock: A primitive blocking mutex.

    let lock = NSLock()
    
    func doSomething() {
        lock.lock()
        // Critical section of code
        lock.unlock()
    }
    
  2. NSRecursiveLock: Allows a thread to acquire the lock multiple times without deadlock.

    let recursiveLock = NSRecursiveLock()
    
    func recursiveFunction(level: Int) {
        recursiveLock.lock()
        if level < 3 {
            recursiveFunction(level: level + 1)
        }
        recursiveLock.unlock()
    }
    
  3. NSCondition: Allows threads to wait for a certain condition before continuing.

    let condition = NSCondition()
    var dataAvailable = false
    
    func producer() {
        condition.lock()
        // Data production
        dataAvailable = true
        condition.signal() // Signal waiting threads
        condition.unlock()
    }
    
    func consumer() {
        condition.lock()
        while !dataAvailable {
            condition.wait() // Wait until condition is met
        }
        // Data processing
        dataAvailable = false
        condition.unlock()
    }
    
  4. NSConditionLock: A mutex that can only be acquired when a certain condition-value is met.

    let conditionLock = NSConditionLock(condition: 0)
    let DATA_READY = 1
    
    func producer() {
        conditionLock.lock(when: 0) // Acquire when condition is 0
        // Data production
        conditionLock.unlock(withCondition: DATA_READY) // Release lock and set condition to DATA_READY
    }
    
    func consumer() {
        conditionLock.lock(when: DATA_READY) // Acquire when condition is DATA_READY
        // Data processing
        conditionLock.unlock(withCondition: 0) // Release lock and set condition to 0
    }
    
  5. Dispatch Queues (GCD): Using serial dispatch queues for access to shared resources.

    let serialQueue = DispatchQueue(label: "com.yourapp.serialQueue")
    var sharedResource = 0
    
    func updateResource() {
        serialQueue.async {
            // Access to sharedResource only from this queue
            sharedResource += 1
        }
    }
    
  6. NSOperationQueue: Allows controlling dependencies and priorities of operations.

    let operationQueue = OperationQueue()
    var sharedOperationResource = 0
    
    let operation1 = BlockOperation {
        // Access to sharedOperationResource
        sharedOperationResource += 1
    }
    
    operationQueue.addOperation(operation1)
    
  7. Actors (Swift 5.5+): Provide state isolation, preventing concurrent access from different contexts.

    actor Counter {
        private var count = 0
    
        func increment() {
            count += 1
        }
    
        func getCount() -> Int {
            return count
        }
    }
    
    let counter = Counter()
    
    Task {
        await counter.increment()
        let currentCount = await counter.getCount()
        print(currentCount)
    }
    
  8. Core Data Contexts: For synchronizing data contexts, hierarchical contexts (parent-child) or mergeChanges(fromContextDidSaveNotification:) can be used.

    // Example with hierarchy (parent - background, child - UI)
    let privateMoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    privateMoc.persistentStoreCoordinator = persistentStoreCoordinator
    
    let mainMoc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    mainMoc.parent = privateMoc
    
    // Example with mergeChanges
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(contextDidSave(_:)),
                                           name: .NSManagedObjectContextDidSave,
                                           object: nil)
    
    @objc func contextDidSave(_ notification: Notification) {
        guard let savingContext = notification.object as? NSManagedObjectContext else { return }
    
        if savingContext != mainMoc { // If save occurred not in main context
            mainMoc.perform {
                mainMoc.mergeChanges(fromContextDidSave: notification)
            }
        }
    }
    
  9. Atomic operations: Using atomic primitives (e.g., from os/lock.h) for simple operations.

The choice of method depends on the complexity of the task, data type, and performance requirements. Actors and GCD are modern preferred approaches.

How to synchronize data between threads (context) in… - sobes.tech