调度组不返回获取的数据

问题描述

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

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

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

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

解决方法

一些问题:

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

  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,error in
                    defer {
                        semaphore.signal()
                        group.leave() // make sure to `leave`,whether successful or not
                    }

                    guard
                        let data = data,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)
        }
    }
}