0

I've been using the CloudKitShare sample code found here as a sample to help me write code for my app. I want to use performWriterBlock and performReaderBlockAndWait as found in BaseLocalCache using a completionHandler without violating the purposes of the design of the code, which focuses on being thread-safe. I include code from CloudKitShare below that are pertinent to my question. I include the comments that explain the code. I wrote comments to identify which code is mine.

I would like to be able to use an escaping completionHandler if possible. Does using an escaping completionHandler still comply with principles of thread-safe code, or does it in any way violate the purpose of the design of this sample code to be thread-safe? If I use an escaping completionHandler, I would need to consider when the completionHandler actually runs relative to other code outside of the scope of the actual perform function that uses the BaseLocalCache perform block. I would for one thing need to be aware of what other code runs in my project between the time the method executes and the time operationQueue in BaseLocalCache actually executes the block of code and thus the completionHandler.

class BaseLocalCache {
    // A CloudKit task can be a single operation (CKDatabaseOperation)
    // or multiple operations that you chain together.
    // Provide an operation queue to get more flexibility on CloudKit operation management.
    //
    lazy var operationQueue: OperationQueue = OperationQueue()
    // This sample ...
    //
    // This sample uses this dispatch queue to implement the following logics:
    // - It serializes Writer blocks.
    // - The reader block can be concurrent, but it needs to wait for the enqueued writer blocks to complete.
    //
    // To achieve that, this sample uses the following pattern:
    // - Use a concurrent queue, cacheQueue.
    // - Use cacheQueue.async(flags: .barrier) {} to execute writer blocks.
    // - Use cacheQueue.sync(){} to execute reader blocks. The queue is concurrent,
    //    so reader blocks can be concurrent, unless any writer blocks are in the way.
    // Note that Writer blocks block the reader, so they need to be as small as possible.
    //
    private lazy var cacheQueue: DispatchQueue = {
        return DispatchQueue(label: "LocalCache", attributes: .concurrent)
    }()
    
    func performWriterBlock(_ writerBlock: @escaping () -> Void) {
        cacheQueue.async(flags: .barrier) {
            writerBlock()
        }
    }
    
    func performReaderBlockAndWait<T>(_ readerBlock: () -> T) -> T {
        return cacheQueue.sync {
            return readerBlock()
        }
    }

}

final class TopicLocalCache: BaseLocalCache {
    
    private var serverChangeToken: CKServerChangeToken?
    
    func setServerChangeToken(newToken: CKServerChangeToken?) {
        performWriterBlock { self.serverChangeToken = newToken }
    }

    func getServerChangeToken() -> CKServerChangeToken? {
        return performReaderBlockAndWait { return self.serverChangeToken }
    }

    // Trial: How to use escaping completionHandler? with a performWriterBlock

    func setServerChangeToken(newToken: CKServerChangeToken?, completionHandler: @escaping (Result<Void, Error>)->Void) {
        performWriterBlock {
            self.serverChangeToken = newToken
            completionHandler(.success(Void()))
        }
    }
    
    // Trial: How to use escaping completionHandler? with a performReaderBlockAndWait

    func getServerChangeToken(completionHandler: (Result<CKServerChangeToken, Error>)->Void) {
        performReaderBlockAndWait {
            if let serverChangeToken = self.serverChangeToken {
                completionHandler(.success(serverChangeToken))
            } else {
                completionHandler(.failure(NSError(domain: "nil CKServerChangeToken", code: 0)))
            }
        }
    }
 
}
daniel
  • 1,446
  • 3
  • 29
  • 65

1 Answers1

1

You asked:

Does using an escaping completionHandler still comply with principles of thread-safe code, or does it in any way violate the purpose of the design of this sample code to be thread-safe?

An escaping completion handler does not violate thread-safety.

That having been said, it does not ensure thread-safety, either. Thread-safety is solely a question of whether you ever access some shared resource from one thread while mutating it from another.

If I use an escaping completionHandler, I would need to consider when the completionHandler actually runs relative to other code outside of the scope of the actual perform function that uses the BaseLocalCache perform block.

Yes, you need to be aware that the escaping completion handler is called asynchronously (i.e., later). That is less of a thread-safety concern than a general understanding of the application flow. It is only a question of what you might be doing in that closure.

IMHO, the more important observation is that the completion handler is called on the cacheQueue used internally by BaseLocalCache. So, the caller needs to be aware that the closure is not called on the caller’s current queue, but on cacheQueue.

It should be noted that elsewhere in that project, they employ another common pattern, where the completion handler is dispatched back to a particular queue, e.g., the main queue.


Bottom line, thread-safety is not a question of whether a closure is escaping or not, but rather (a) from what thread does the method call the closure; and (b) what the supplied closure actually does:

  • Do you interact with the UI? Then you will want to ensure that you dispatch that back to the main queue.

  • Do you interact with your own properties? Then you will want to make sure you synchronize all of your access with them, either with actors, relying on the main queue, use your own serial queues, or a reader-writer pattern like in the example you shared with us.

If you are ever unsure about your code’s thread-safety, you might consider temporarily turning on TSAN as described in Diagnosing Memory, Thread, and Crash Issues Early

Rob
  • 415,655
  • 72
  • 787
  • 1,044