【问题标题】:How do I write thread-safe code that uses a completionHandler with a function that delegates code to an instance of OperationQueue?如何编写使用completionHandler 的线程安全代码以及将代码委托给OperationQueue 实例的函数?
【发布时间】:2022-09-26 16:38:14
【问题描述】:

我一直在使用找到的 CloudKitShare 示例代码 here 作为示例来帮助我为我的应用程序编写代码。我想使用在 BaseLocalCache 中找到的 performWriterBlock 和 performReaderBlockAndWait 使用 completionHandler 而不违反代码设计的目的,该目的侧重于线程安全。我在下面包含了与我的问题相关的 CloudKitShare 代码。我包括解释代码的 cmets。我写了 cmets 来识别哪个代码是我的。

如果可能的话,我希望能够使用转义的完成处理程序。使用转义的 completionHandler 是否仍然符合线程安全代码的原则,或者它是否以任何方式违反了此示例代码设计为线程安全的目的?如果我使用转义的完成处理程序,我需要考虑何时完成处理程序相对于使用 BaseLocalCache 执行块的实际执行功能范围之外的其他代码实际运行。一方面,我需要知道在方法执行时间和 BaseLocalCache 中的 operationQueue 实际执行代码块以及因此完成处理程序之间,我的项目中运行了哪些其他代码。

更新:

我补充说,我在最后编写的带有 \"// Trial: ...\" 标记的试用代码在代码时间期间会生成一条错误消息,上面写着 \"Type of expression is ambiguous without more context\"。当我删除内部花括号之间的代码并因此不发送任何代码作为 performReaderBlockAndWait(:) BaseLocalCache 方法,错误信息消失。我发现这是因为 performReaderBlockAndWait(:) 方法不处理我的试用代码 getServerChangeToken(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 ...
    //
    // 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)))
            }
        }
    }
 
}

    标签: ios swift concurrency thread-safety swift-concurrency


    【解决方案1】:

    您问:

    使用转义 completionHandler 是否仍然符合线程安全代码的原则,或者它是否以任何方式违反了此示例代码设计为线程安全的目的?

    转义完成处理程序不会违反线程安全。

    话虽如此,它也不能确保线程安全。线程安全只是一个问题,即您是否曾经从一个线程访问某些共享资源,同时又从另一个线程对其进行变异。

    如果我使用转义completionHandler,我需要考虑completionHandler 何时实际运行相对于使用BaseLocalCache 执行块的实际执行函数范围之外的其他代码。

    是的,您需要注意转义完成处理程序是异步调用的(即稍后)。与对应用程序流的一般理解相比,这不是线程安全问题。这只是你在那个闭包中可能会做什么的问题。

    恕我直言,更重要的观察是完成处理程序是在BaseLocalCache 内部使用的cacheQueue 上调用的。因此,调用者需要知道闭包不是在调用者的当前队列上调用的,而是在cacheQueue 上调用的。

    应该注意的是,在该项目的其他地方,他们采用了另一种常见的模式,其中完成处理程序被分派回特定队列,例如主队列。


    归根结底,线程安全不是闭包是否正在转义的问题,而是(a)方法从哪个线程调用闭包; (b) 提供的闭包实际上做了什么:

    • 您是否与 UI 交互?然后,您将要确保将其分派回主队列。

    • 您是否与自己的属性进行交互?然后,您将需要确保将所有访问与他们同步,无论是与参与者、依赖于主队列、使用您自己的串行队列,还是像您与我们共享的示例中那样的读写器模式。

    如果您不确定您的代码的线程安全性,您可以考虑暂时打开 TSAN,如 Diagnosing Memory, Thread, and Crash Issues Early 中所述

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-08
      • 2014-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多