防止完成处理程序同步执行

时间:2017-11-23 14:53:31

标签: arrays swift multithreading networking concurrency

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

我想确保在某些情况下线程无法访问阵列。例如,请考虑以下情形:

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

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

3 个答案:

答案 0 :(得分:2)

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

您想要的伪代码就像

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的应用程序都是如此,但对于命令行工具则不一定如此。如果是这种情况或者您不希望在主线程上发生任何工作,请设置一个串行队列并在其上运行连接启动和完成处理程序而不是主队列。

答案 1 :(得分:1)

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

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

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

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.
    }
} 

这是如此广泛适用,你实际上可以将回调存储,锁定和调用提取到它自己的通用组件中,这是一个"请求者"类型可以创建,拥有和使用。

答案 2 :(得分:1)

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

请求者类管理一系列请求。例如,可以选择T = UIImage,ID =图像URL。这会给我们一个图像下载器。或者可以选择T = 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)
        }
    }
}