【问题标题】:Preventing completion handler to be executed synchronously防止完成处理程序同步执行
【发布时间】:2017-11-23 14:53:31
【问题描述】:

我正在用 Swift 编写一些网络代码,以防止启动已经在进行中的下载。我通过在(同步)数组A 中跟踪网络请求的身份以及相关的完成处理程序来做到这一点。当网络调用完成时,它会调用与该资源关联的完成处理程序,然后从数组A 中删除这些处理程序。

我想确保在某些情况下线程无法访问数组。例如,考虑以下场景:

  1. 开始请求下载资源X
  2. 验证请求是否已经发出。
  3. 将完成处理程序添加到数组A
  4. 如果尚未提出请求,请开始下载。

如果资源X 已经在下载,并且该下载的完成处理程序中断了步骤 2 和 3 之间的线程,该怎么办?已验证请求已发出,因此不会开始下载,但新的完成处理程序将添加到数组A,现在将永远不会被调用。

我将如何阻止这种情况发生?我可以在执行第 2 步和第 3 步时锁定数组以进行写入吗?

【问题讨论】:

  • “在(同步的)数组中跟踪网络请求的身份以及相关的完成处理程序”......这不正是 DispatchQueue 的内容吗?只需在串行调度队列中提交您的作业
  • What if resource X was already downloading, and the completion handler for this download interrupts the thread between steps 2 and 3?。我可能会感到困惑,但是当 X 仍在下载时,完成处理程序如何运行?完成处理程序不是在 X 完成后运行吗?
  • @John,我希望所有想要下载资源 X 的处理程序都被触发。发出请求的对象应该不知道这种机制,并且只要触发它们的完成处理程序就好像它们启动了下载一样,即使另一个对象首先发出请求。
  • @TNguyen:资源 X 下载可能已经启动。然后,当另一个对象请求资源 X 时,可以在第 2 步和第 3 步之间触发先前下载的完成处理程序。
  • 与其使它们可取消,不如考虑让客户端对象在它们决定不需要结果时忽略回调。另外,感谢您分享您的想法,但请将其放入答案中,而不是问题中。

标签: arrays swift multithreading networking concurrency


【解决方案1】:

简单的解决方案是在主线程上运行除实际下载之外的所有内容。您需要做的就是使完成处理程序成为一个存根,在主队列上放置一个块来完成所有工作。

你想要的伪代码类似于

assert(Thread.current == Thread.main)
handlerArray.append(myHandler)
if !requestAlreadyRunning)
{
    requestAlreadyRunning = true
    startDownloadRequest(completionHandelr: {
        whatever in
        Dispatch.main.async // This is the only line of code that does not run on the main thread
        {
            for handler in handlerArray
            { 
                handler()
            }
            handlerArray = []
            requestAlreadyRunning = false
        }
    })
}

这是可行的,因为所有可能导致竞争条件和同步冲突的工作都在一个线程上运行 - 主线程,因此当您向队列添加新的完成处理程序时,完成处理程序不可能运行,反之亦然.

请注意,要使上述解决方案发挥作用,您的应用程序需要处于运行循环中。这对于 Mac OS 或 iOS 上的任何基于 Cocoa 的应用程序都是如此,但对于命令行工具则不一定如此。如果是这种情况,或者您不希望任何工作发生在主线程上,请设置一个串行队列并在其上运行连接启动和完成处理程序,而不是主队列。

【讨论】:

    【解决方案2】:

    我正在假设您希望能够添加多个回调,这些回调都将在最新请求完成时运行,无论它是否已经在进行中。

    这是一个解决方案的草图。基本点是在接触处理程序数组之前获取锁,无论是添加一个还是在请求完成后调用它们。您还必须同步确定是否启动新请求,使用完全相同的锁

    如果锁已经在添加处理程序的公共方法中持有,并且请求自己的完成运行,那么后者必须等待前者,并且您将具有确定性行为(将调用新的处理程序)。

    class WhateverRequester
    {
        typealias SuccessHandler = (Whatever) -> Void
        typealias FailureHandler = (Error) -> Void
    
        private var successHandlers: [SuccessHandler] = []
        private var failureHandlers: [FailureHandler] = []
    
        private let mutex = // Your favorite locking mechanism here.
    
        /** Flag indicating whether there's something in flight */
        private var isIdle: Bool = true
    
        func requestWhatever(succeed: @escaping SuccessHandler,
                             fail: @escaping FailureHandler)
        {
            self.mutex.lock()
            defer { self.mutex.unlock() }
    
            self.successHandlers.append(succeed)
            self.failureHandlers.append(fail)
    
            // Nothing to do, unlock and wait for request to finish
            guard self.isIdle else { return }
    
            self.isIdle = false
            self.enqueueRequest()
        }
    
        private func enqueueRequest()
        {
            // Make a request however you do, with callbacks to the methods below
        }
    
        private func requestDidSucceed(whatever: Whatever)
        {
            // Synchronize again before touching the list of handlers and the flag
            self.mutex.lock()
            defer { self.mutex.unlock() }
    
            for handler in self.successHandlers {
                handler(whatever)
            }
    
            self.successHandlers = []
            self.failureHandlers = []
            self.isIdle = true
        }
    
        private func requestDidFail(error: Error)
        {
            // As the "did succeed" method, but call failure handlers
            // Again, lock before touching the arrays and idle flag.
        }
    } 
    

    这非常适用,您实际上可以将回调存储、锁定和调用提取到它自己的通用组件中,“请求者”类型可以创建、拥有和使用这些组件。

    【讨论】:

    • 谢谢。这很有帮助。选择一种锁定机制而不是另一种是否有一些含义? NSLock 在这种情况下会做吗?我今天将实现你的代码,并根据你的代码用我的(通用)实现来更新我的问题。
    • requestDidSucceed 中的锁可能在 requestWhatever 运行所在的不同线程上调用是否重要? (我假设不是)。与创建串行队列并在该串行队列上执行 requestWhatever 和 requestDidSucceed 方法相比,是否更倾向于使用 NSLock? (正如@JeremyP 所建议的那样。)
    • “requestDidSucceed 中的锁可能在不同的线程上被调用是否重要” 不,这正是它的用途。如果锁没有阻止多线程访问,那么它就不会是一个锁。通过串行队列同步也是一种选择,如果您愿意,可以将其转换为使用一个,但是您必须在 requestDidSucceedrequestDidFail 方法中排队跳跃,以确保处理程序位于预期的线程上他们的主人。
    • 我用请求的通用实现更新了我的答案。我还发现我需要一个请求者来跟踪我的各种请求。我正在努力寻找正确的设计模式。在某些时候,我遇到了一个问题,即我无法在请求中使用协议来告诉请求者它需要从数组中删除自己,因为我使用的是泛型类型。我最终通过了一个关闭。不太漂亮,但至少它在界面中隐藏了。
    • 另外,我对我发布的代码中的一些“应该”不太满意。我想保护自己不要忘记做这个或那个。因为我希望“工作”非常通用,所以我没有看到任何方法来实现我想要的,而不需要在工作关闭中进行一些手动回调。
    【解决方案3】:

    根据 Josh 的回答,我在下面创建了一个通用的 Request & Requester。它有一些比我在上面问题中描述的更具体的需求。我希望 Request 实例只管理具有特定 ID 的请求(我现在将其制成一个字符串,但我想这也可以使其更通用)。不同的 ID 需要不同的 Request 实例。为此,我创建了 Requester 类。

    请求者类管理一组请求。例如,可以选择 T = UIImage,ID = 图片 URL。这会给我们一个图像下载器。或者可以选择 T = User,ID = user id。即使多次请求,这也只会获得一次用户对象。

    我还希望能够取消来自单个呼叫者的请求。它使用一个唯一的 ID 标记完成处理程序,该 ID 将传递回调用者。它可以使用它来取消请求。如果所有调用者都取消,则从请求者中删除请求。

    (以下代码未经测试,因此我不能保证它没有错误。使用风险自负。)

    import Foundation
    
    typealias RequestWork<T> = (Request<T>) -> ()
    typealias RequestCompletionHandler<T> = (Result<T>) -> ()
    typealias RequestCompletedCallback<T> = (Request<T>) -> ()
    
    struct UniqueID {
        private static var ID: Int = 0
        static func getID() -> Int {
            ID = ID + 1
            return ID
        }
    }
    
    enum RequestError: Error {
        case canceled
    }
    
    enum Result<T> {
        case success(T)
        case failure(Error)
    }
    
    protocol CancelableOperation: class {
        func cancel()
    }
    
    final class Request<T> {
        private lazy var completionHandlers = [(invokerID: Int,
                                                completion: RequestCompletionHandler<T>)]()
        private let mutex = NSLock()
        // To inform requester the request has finished
        private let completedCallback: RequestCompletedCallback<T>!
        private var isIdle = true
        // After work is executed, operation should be set so the request can be
        // canceled if possible
        var operation: CancelableOperation?
        let ID: String!
    
        init(ID: String,
             completedCallback: @escaping RequestCompletedCallback<T>) {
            self.ID = ID
            self.completedCallback = completedCallback
        }
    
        // Cancel the request for a single invoker and it invokes the competion
        // handler with a cancel error. If the only remaining invoker cancels, the
        // request will attempt to cancel
        // the associated operation.
        func cancel(invokerID: Int) {
            self.mutex.lock()
            defer { self.mutex.unlock() }
            if let index = self.completionHandlers.index(where: { $0.invokerID == invokerID }) {
                self.completionHandlers[index].completion(Result.failure(RequestError.canceled))
                self.completionHandlers.remove(at: index)
                if self.completionHandlers.isEmpty {
                    self.isIdle = true
                    operation?.cancel()
                    self.completedCallback(self)
                }
            }
        }
    
        // Request work to be done. It will only be done if it hasn't been done yet.
        // The work block should set the operation on this request if possible. The
        // work block should call requestFinished(result:) if the work has finished.
        func request(work: @escaping RequestWork<T>,
                     completion: @escaping RequestCompletionHandler<T>) -> Int {
            self.mutex.lock()
            defer { self.mutex.unlock() }
            let ID = UniqueID.getID()
            self.completionHandlers.append((invokerID: ID, completion: completion))
            guard self.isIdle else { return ID }
            work(self)
            self.isIdle = false
            return ID
        }
    
        // This method should be called from the work block when the work has
        // completed. It will pass the result to all completion handlers and call
        // the Requester class to inform that this request has finished.
        func requestFinished(result: Result<T>) {
            self.mutex.lock()
            defer { self.mutex.unlock() }
            completionHandlers.forEach { $0.completion(result) }
            completionHandlers = []
            self.completedCallback(self)
            self.isIdle = true
        }
    }
    
    final class Requester<T>  {
        private lazy var requests = [Request<T>]()
        private let mutex = NSLock()
    
        init() { }
    
        // reuqestFinished(request:) should be called after a single Request has
        // finished its work. It removes the requests from the array of requests.
        func requestFinished(request: Request<T>) {
            self.mutex.lock()
            defer { self.mutex.unlock() }
            if let index = requests.index(where: { $0.ID == request.ID }) {
                requests.remove(at: index)
            }
        }
    
        // request(ID:, work:) will create a request or add a completion handler to
        // an existing request if a request with the supplied ID already exists.
        // When a request is created, it passes a closure that removes the request.
        // It returns the invoker ID to the invoker for cancelation purposes.
        func request(ID: String,
                     work: @escaping RequestWork<T>,
                     completion: @escaping RequestCompletionHandler<T>) ->
            (Int, Request<T>) {
            self.mutex.lock()
            defer { self.mutex.unlock() }
            if let existingRequest = requests.first(where: { $0.ID == ID }) {
                let invokerID = existingRequest.request(work: work, completion: completion)
                return (invokerID, existingRequest)
            } else {
                let request = Request<T>(ID: ID) { [weak self] (request) in
                    self?.requestFinished(request: request)
                }
                let invokerID = request.request(work: work, completion: completion)
                return (invokerID, request)
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-13
      • 2019-11-16
      相关资源
      最近更新 更多