如何执行多个异步请求,一个接一个地开始

4

我有一组异步执行的请求。然而,每个下一个请求只有在前一个请求完成后才能开始(由于数据依赖性)。

由于所有请求都应按正确顺序完成,DispatchGroup()似乎没有用。

我目前实施了DispatchSemaphore(),但我觉得这不是最好的解决方案,因为我想确保所有请求都在后台执行。

let semaphore = DispatchSemaphore(value: requests.count)

for request in requests {
    apiManager().performAsyncRequest(request, failure: { error in
        print(error); semaphore.signal()
        }) { print(“request finished successful”) 
        // Next request should be performed now
        semaphore.signal()
    }
}
semaphore.wait()

有更好的方法来执行这个吗?

注意: 根据下面某个答案的实现,我遇到了 apiManager() 不是线程安全的问题 (由于使用了 Realm 数据库)。

为了保持这个问题的清晰,并考虑到以线程安全方式考虑答案,需要一个线程安全定义的 performAsyncRequest

public func performAsyncRequest(_ requestNumber: Int, success: @escaping (Int) -> Void)->Void {
    DispatchQueue(label: "performRequest").async {
        usleep(useconds_t(1000-requestNumber*200))
        print("Request #\(requestNumber) starts")
        success(requestNumber)
    }
}

使用DispatchSemaphore的解决方案

        let semaphore = DispatchSemaphore(value: 1)
        DispatchQueue(label: "requests").async {
            for requestNumber in 0..<4 {
                semaphore.wait()
                performAsyncRequest(requestNumber) { requestNumber in
                        print("Request #\(requestNumber) finished")
                        semaphore.signal()
                }
            }
        }

应输出如下结果:

Request #0 starts
Request #0 finished
Request #1 starts
Request #1 finished
Request #2 starts
Request #2 finished
Request #3 starts
Request #3 finished

操作失败,错误信息为Operation

    var operations = [Operation]()

    for requestNumber in 0..<4 {
        let operation = BlockOperation(block: {
            performAsyncRequest(requestNumber) { requestNumber in
                DispatchQueue.main.sync {
                    print("Request #\(requestNumber) finished")
                }
            }
        })

        if operations.count > 0 {
            operation.addDependency(operations.last!)
        }
        operations.append(operation)
    }

    let operationQueue = OperationQueue.main
    operationQueue.addOperations(operations, waitUntilFinished: false)

输出不正确的情况:

Request #0 starts
Request #1 starts
Request #2 starts
Request #3 starts
Request #0 finished
Request #3 finished
Request #2 finished
Request #1 finished

我个人认为,使用 Operation 也应该可以实现这个功能,但我不确定它是否比使用 DispatchSemaphore 更好。


你正在针对哪个版本的Swift进行开发? - xpereta
Swift 3。我刚刚使用DispatchSemaphore实现了问题的编辑,但我想知道在这种情况下是否应该使用它。 - Taco
“所有请求都在后台执行”是什么意思?如果 apiManager() 不是线程安全的,那么在后台线程上执行 performAsyncRequest 调用将不可能。 - xpereta
我想说的是“所有请求都是异步执行的,不会阻塞用户界面”。由于apiManager()内部执行了一个Alamofire请求,因此我认为这些请求是在后台线程中执行的。然而,从你的回答实现中可以看出,获取和处理Realm数据应该从主线程中获取。 - Taco
请您在另一个问题中解决线程安全问题,让这个问题来处理异步调用的串行执行。这样回答将更有用和清晰,方便未来的用户。 - xpereta
2个回答

1
你可以使用NSOperationQueue,将每个请求作为操作添加。
let firstOperation: NSOperation
let secondOperation: NSOperation
secondOperation.addDependency(firstOperation)

let operationQueue = NSOperationQueue.mainQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)

这个答案如何解决一个异步调用未完成之前不会启动另一个调用的要求? - xpereta
@xpereta NSOperationQueue 支持依赖关系,在这里我让 secondOperation 依赖于第一个操作,这样在第一个操作完成前它不会启动。 https://developer.apple.com/reference/foundation/operation - Misha
在这个问题中,对 performAsyncRequest 的调用是异步的,并且将立即完成。直到前一个请求的完成闭包被执行之后,才能开始下一个请求。 - xpereta
@xpereta 这是针对之前的问题的回答,是的,我相信我们可以进行异步操作。 - Misha
我无法正确使用(NS)Operation(因此接受了其他答案),但我认为这也应该可以。我在问题中更新了一个在更简单(线程安全)的情况下使用Operation的实现,但似乎请求的处理顺序不正确。你知道我做错了什么吗?你可能还知道为什么这种方法比使用DispatchSemaphore更好吗?不幸的是,在我的真实应用程序中,我也遇到了DispatchSemaphore的问题,因此我提出了一个新问题。非常感谢... - Taco
我的新问题已发布在这里:http://stackoverflow.com/questions/39898969/how-to-perform-asynchronous-requests-serially-in-combination-with-realm-and-alam?noredirect=1#comment67084647_39898969 - Taco

1
你使用DispatchSemaphore是正确的,可以确保在上一个异步调用完成之前不会开始下一个。我建议只需确保管理异步API调用的代码在后台运行:
let backgroundQueue = DispatchQueue(label: "requests")
let semaphore = DispatchSemaphore(value: 1)

backgroundQueue.async {
    var requestNumber = 1

    for request in requests {
        semaphore.wait()

        let currentRequestNumber = requestNumber

        print("Request launched #\(requestNumber)")

        apiManager().performAsyncRequest(request,
        failure: {
            error in
            print("Request error #\(currentRequestNumber)")
            semaphore.signal()
        }) {
            print("Request result #\(currentRequestNumber)")
            semaphore.signal()
        }

        requestNumber = requestNumber + 1
    }
}

代码将立即继续执行,而for循环在后台循环运行,并在等待前一个请求完成后开始每个请求。
或者,如果apiManager()不是线程安全的:
let semaphore = DispatchSemaphore(value: 1)

var requestNumber = 1

for request in requests {
    semaphore.wait()

    let currentRequestNumber = requestNumber

    print("Request launched #\(requestNumber)")

    apiManager().performAsyncRequest(request,
    failure: {
        error in
        print("Request error #\(currentRequestNumber)")
        semaphore.signal()
    }) {
        print("Request result #\(currentRequestNumber)")
        semaphore.signal()
    }

    requestNumber = requestNumber + 1
}

这种方式的限制是for循环会一直执行,直到最后一个请求开始执行。但是,如果您调用的代码不是线程安全的,那么就没有办法避免这种情况。


很遗憾,我无法让它正常工作。由于底层的 Realm 数据库,出现了以下错误:terminating with uncaught exception of type realm::IncorrectThreadException: Realm accessed from incorrect thread. 然而,如果不使用后台线程,实现将导致死锁。 - Taco
目前,我也正在使用其他答案中建议的NSOperation进行实现。无论如何,这种用例难道不更适合于NSOperation吗? - Taco
请修改问题以澄清apiManager不是线程安全的,参见:https://dev59.com/0F8e5IYBdhLWcg3whqrl#25670511。 - xpereta
请参见我对那个答案的评论。 - xpereta
我之前没有意识到线程不安全的问题,但我会更新这个问题。嗯...它变得比预期的要复杂一些。有什么解决这个问题的想法吗?非常感谢你的帮助... - Taco
你的代码中是否实现了单例模式,或者创建了一个 Realm 对象并将其用于多个请求?你的问题可能就出在这里。在链接答案中的示例中,请注意解决方案是如何为每个异步线程创建一个新对象的。如果您存储了一个 Realm 对象以供所有请求重复使用,如果所有代码都在同一线程中运行,则可能不会遇到任何问题,但现在似乎您正在一个线程中创建 Realm 对象,然后从新的后台线程访问它。 - xpereta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接