【问题标题】:Dispatch group don't return fetched data调度组不返回获取的数据
【发布时间】:2021-01-23 20:49:52
【问题描述】:

我正在尝试使用 DispatchGroup 从多个请求中获取数据。 我不明白为什么 print(weatherData.fact.pressureMm!) 正在工作,但数据没有附加到 dataArray 和 print(dataArray?[0].fact.pressureMm ?? "nil") print nil.

我也在尝试从 complitionHandeler 打印数据,结果是一样的。

如何在数组中附加 weatherData 并正确地从编译中获取值?

func fetchWeatherForCities (complitionHandeler: @escaping([YandexWeatherData]?)->Void) {
    var dataArray: [YandexWeatherData]?

    let group = DispatchGroup()

    for city in cities {
        group.enter()
        DispatchQueue.global().async {

            var urlString = self.urlString

            self.locationManager.getCoordinate(forCity: city) { (coordinate) in

                urlString += self.latitudeField + coordinate.latitude
                urlString += self.longitudeField + coordinate.longitude

                guard let url = URL(string: urlString) else {return}
                var request = URLRequest(url: url)
                request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)


                let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
                    if let error = error {
                        print(error)
                    }

                    if let data = data {
                        guard let weatherData = self.parseJSON(withData: data) else {return}
                        print(weatherData.fact.pressureMm!)
                        dataArray?.append(weatherData)
                        print(dataArray?[0].fact.pressureMm ?? "nil")
                        group.leave()
                    }
                }
                dataTask.resume()
            }
        }
    }
    group.notify(queue: DispatchQueue.global()) {
        complitionHandeler(dataArray)
    }
}

【问题讨论】:

  • 什么是parseJSON?您有太多 unmanaged 退出,其中没有留下 groupdataArray 已声明但没有任何价值。为什么它是可选的?
  • 您错误地使用了 DispatchGroup。
  • @vadian 在我将空值设置为数组后它开始工作。
  • @matt 你能解释一下如何使它正确吗?

标签: swift grand-central-dispatch dispatchgroup


【解决方案1】:

几个问题:

  1. 您有执行路径,如果发生错误,您将不会调用leave。确保每条执行路径,包括每条“提前退出”,都用leave 偏移enter

  2. 您将dataArray 定义为可选项,但从不初始化它。因此它是nil。而dataArray?.append(weatherData) 因此永远不会附加值。

因此,也许:

func fetchWeatherForCities (completionHandler: @escaping ([YandexWeatherData]) -> Void) {
    var dataArray: [YandexWeatherData] = []
    let group = DispatchGroup()

    for city in cities {
        group.enter()

        var urlString = self.urlString

        self.locationManager.getCoordinate(forCity: city) { (coordinate) in
            urlString += self.latitudeField + coordinate.latitude
            urlString += self.longitudeField + coordinate.longitude

            guard let url = URL(string: urlString) else {
                group.leave()     // make sure to `leave` in early exit
                return
            }

            var request = URLRequest(url: url)
            request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)

            let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
                guard
                    let data = data,
                    error == nil,
                    let weatherData = self.parseJSON(withData: data)
                else {
                    group.leave() // make sure to `leave` in early exit
                    print(error ?? "unknown error")
                    return
                }

                print(weatherData.fact.pressureMm!) // I'd advise against every doing force unwrapping on results from a third party service
                dataArray.append(weatherData)

                group.leave()
            }
            dataTask.resume()
        }
    }

    group.notify(queue: .main) {
        completionHandler(dataArray)
    }
}

顺便说一句,在上面,我做了两个不相关的 GCD 更改,即:

  • 删除了将网络请求分派到全局队列。网络请求已经是异步的,所以分派请求的创建和请求的开始有点多余。

  • 在您的 notify 块中,您使用的是全局队列。如果您确实需要,您当然可以这样做,但很可能您将更新模型对象(如果您从后台队列执行此操作,则需要同步)和 UI 更新。如果您将其分派到主队列,生活会更轻松。


FWIW,当您解决当前问题时,您可能需要考虑另外两件事:

  1. 如果检索多个位置的详细信息,您可能希望将其限制为一次仅运行一定数量的请求(并避免后一个请求超时)。一种方法是使用非零信号量:

    DispatchQueue.global().async {
        let semaphore = DispatchSemaphore(value: 4)
    
        for i in ... {
            semaphore.wait()
    
            someAsynchronousProcess(...) {
                ...
    
                semaphore.signal()
            }
        }
    }
    

    如果您过去使用过信号量,这可能会感觉倒退(在发出信号之前等待;哈哈),但非零信号量会让其中四个启动,而其他信号量将作为前四个单独完成/信号启动。

    另外,因为我们现在正在等待,所以我们必须将调度重新引入后台队列以避免阻塞。

  2. 并发运行异步请求时,它们可能不会按照您启动它们的顺序完成。如果您希望它们以相同的顺序排列,一种解决方案是在结果完成时将结果存储在字典中,然后在 notify 块中构建结果的排序数组:

    var results: [Int: Foo] = [:]
    
    // start all the requests, populating a dictionary with the results
    
    for (index, city) in cities.enumerated() {
        group.enter()
        someAsynchronousProcess { foo in
            results[i] = foo
            group.leave()
        }
    }
    
    // when all done, build an array in the desired order
    
    group.notify(queue: .main) {
        let array = self.cities.indices.map { results[$0] } // build sorted array of `[Foo?]`
        completionHandler(array)
    }
    

    这就引出了关于如何处理错误的问题,因此您可以将其设为一个可选数组(如下所示)。

也许把它们放在一起:

func fetchWeatherForCities(completionHandler: @escaping ([YandexWeatherData?]) -> Void) {
    DispatchQueue.global().async {
        var results: [Int: YandexWeatherData] = [:]
        let semaphore = DispatchSemaphore(value: 4)
        let group = DispatchGroup()

        for (index, city) in self.cities.enumerated() {
            group.enter()
            semaphore.wait()

            var urlString = self.urlString

            self.locationManager.getCoordinate(forCity: city) { coordinate in
                urlString += self.latitudeField + coordinate.latitude
                urlString += self.longitudeField + coordinate.longitude

                guard let url = URL(string: urlString) else {
                    semaphore.signal()
                    group.leave()     // make sure to `leave` in early exit
                    return
                }

                var request = URLRequest(url: url)
                request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)

                let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
                    defer {
                        semaphore.signal()
                        group.leave() // make sure to `leave`, whether successful or not
                    }

                    guard
                        let data = data,
                        error == nil,
                        let weatherData = self.parseJSON(withData: data)
                    else {
                        print(error ?? "unknown error")
                        return
                    }

                    results[index] = weatherData
                }
                dataTask.resume()
            }
        }

        group.notify(queue: .main) {
            let array = self.cities.indices.map { results[$0] } // build sorted array
            completionHandler(array)
        }
    }
}

【讨论】:

  • 我对信号量的这种使用特别感兴趣,这是我从未想到过的。
  • 我个人会使用合理的maxConcurrentOperationCount 或将URLSession 发布者与maxPublishers 组合在操作队列上使用异步操作,但我决定在这里使用非零的老学校信号。我担心我已经用太多的新概念对 OP 征税了。大声笑。