如何编写带逃逸completionHandler的线程安全OperationQueue相关代码?
问题描述
我基于苹果开发者官网的CloudKitShare示例代码开发应用,希望在BaseLocalCache的performWriterBlock和performReaderBlockAndWait方法中加入completionHandler,同时不破坏原有的线程安全设计。我尝试编写了带逃逸(@escaping)属性的completionHandler方法,需要明确以下问题:
- 使用逃逸completionHandler是否仍符合线程安全原则,会不会违背示例的线程安全设计目标?
- 如果可以使用,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 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))) } } } }
解答
一、逃逸CompletionHandler是否符合线程安全原则?
完全符合,不会破坏原有的线程安全设计。原示例的线程安全核心逻辑未被改变:
- 写操作依然通过
cacheQueue.async(flags: .barrier)串行执行,保证同一时间只有一个写操作,且会阻塞所有读操作直到完成。 - 读操作依然通过
cacheQueue.sync执行,无写操作时可并发执行,有写操作时会等待写操作完成。
completionHandler只是在读写操作完成后触发的回调,缓存的访问仍严格限制在cacheQueue上,线程安全的核心规则没有被打破。
二、CompletionHandler的执行时机与外部代码的关系
写操作的completionHandler:
在setServerChangeToken(newToken:completionHandler:)中,completionHandler在cacheQueue的barrier异步块内执行,会在写操作(self.serverChangeToken = newToken)完成后,于cacheQueue的后台线程触发。外部调用该方法的线程不会被阻塞,因为performWriterBlock本身是异步执行的。读操作的completionHandler:
在getServerChangeToken(completionHandler:)中,completionHandler在cacheQueue.sync的同步块内执行。由于performReaderBlockAndWait是同步方法,外部调用线程会被阻塞,直到读操作和completionHandler全部执行完成,且completionHandler是在cacheQueue的线程上执行,而非外部调用线程。
三、使用时的注意事项
- 禁止在completionHandler中直接操作UI:completionHandler运行在
cacheQueue后台线程,iOS要求UI操作必须在主线程执行,需手动切换线程:completionHandler(.success(serverChangeToken)) DispatchQueue.main.async { // 这里执行UI更新逻辑 } - 避免在completionHandler中嵌套调用缓存方法:如果在回调里再次调用
setServerChangeToken或getServerChangeToken,可能引发队列嵌套,极端情况下会导致死锁(比如同步读的回调里调用同步读)。 - 统一逃逸属性标记:写操作的completionHandler加了
@escaping是合理的(异步块执行,生命周期超过方法本身);读操作的completionHandler目前未加,但如果后续将读操作改为异步,就需要添加@escaping,建议统一添加保持代码一致性。 - 错误处理需贴合业务场景:读操作中把
serverChangeToken为nil视为错误,需根据实际业务判断是否合理——如果nil是合法状态,应返回.success(nil)而非错误。
内容的提问来源于stack exchange,提问作者daniel

