Swift: невозможно связать DispatchGroup - PullRequest
0 голосов
/ 02 сентября 2018

Я пытаюсь связать две группы рассылки, но мой код всегда падает в случайных местах.

Моя функция такова:

func getAllActivities(userUID: String, _ completionHandler: @escaping (_ activities: [ActivitiesForUIStruct]?, _ error: Error?) -> Void) {
    let downloadGroup = DispatchGroup()
    let detailGroup = DispatchGroup()

    let _ = DispatchQueue.global(qos: .userInitiated)

    var activities = [ActivitiesForUIStruct]()

    getActivities(userUID: userUID) { (results, error) in
        //Handle error

        downloadGroup.enter()
        if let results = results {
            var groupTitle = ""
            var originatorName = ""
            var sourceName = ""

            results.forEach({ (activity) in
                let originatorUID = activity.originatorUserUID ?? ""
                let groupID = activity.inspectionGroupID ?? ""
                let sourceID = activity.sourceID ?? ""

                detailGroup.enter()
                self.getTitle(key: "inspectionGroups", value: groupID, { (title, error) in
                    ...

                    if let title = title {
                        groupTitle = title
                        detailGroup.leave()
                    }
                })

                detailGroup.enter()
                self.getName(userUID: originatorUID, { (name, error) in
                    ...

                    if let name = name {
                        originatorName = name
                        detailGroup.leave()
                    }
                })

                detailGroup.enter()
                self.getTitle(key: "users", value: sourceID, { (title, error) in
                    ... 

                    if let title = title {
                        sourceName = title
                        detailGroup.leave()
                    }
                })

                detailGroup.notify(queue: .global(qos: .userInitiated), execute: {
                    let activityUI = ActivitiesForUIStruct(activities: activity, originatorName: originatorName, sourceName: sourceName, inspectionGroupTitle: groupTitle)
                    activities.append(activityUI) //crash here
                    downloadGroup.leave() //crash here
                })
            })
        }

        downloadGroup.notify(queue: .main, execute: {
            completionHandler(activities, nil)
        })
    }
}

Цель моего кода - извлечь имя и заголовок из UID, а затем добавить его к пользовательскому struct. Тем не менее, моя реализация имеет тенденцию к краху в областях, отмеченных detailGroup.notify. Кто-нибудь посоветует, где я ошибся?

Примечание: мои вызовы функций верны, т.е. я могу распечатать originatorName, sourceName и groupTitle.

1 Ответ

0 голосов
/ 02 сентября 2018

Во-первых,

вам нужно сбалансировать enter() и leave(). Ваш код вызывает downloadGroup.enter() только один раз из results.forEach, но downloadGroup.leave() вызывается внутри цикла несколько раз. Так, если results имеет два или более элементов, downloadGroup.leave() вызывается больше раз, чем downloadGroup.enter().


Во-вторых,

Некоторые, казалось бы, асинхронные методы могут вызывать обработчик завершения синхронно. (Например, при попадании в кеш.) Итак, некоторые detailGroup.leave() могут быть вызваны до detailGroup.enter().

detailGroup.enter() +1
detailGroup.leave() 0  --> detailGroup.notify fires!
detailGroup.enter() +1
detailGroup.leave() 0  --> detailGroup.notify fires!
detailGroup.enter() +1
detailGroup.leave() 0  --> detailGroup.notify fires!

(detailGroup.notify срабатывает каждый раз, когда внутренний счетчик получает 0.)

Как показано выше, это может вызвать detailGroup.notify возгорание несколько раз. Чтобы заставить detailGroup.notify стрелять только один раз, когда все задачи завершены, вам нужно гарантировать все detailGroup.enter(), выполненные до любого detailGroup.leave().

detailGroup.enter() +1
detailGroup.enter() +2
detailGroup.enter() +3
detailGroup.leave() +2
detailGroup.leave() +1
detailGroup.leave() 0  --> detailGroup.notify fires!

В-третьих,

Swift Array не является поточно-ориентированным. Таким образом, если ваш activities может быть обновлен в несколько потоков, это может привести к неожиданному поведению, включая сбой. Лучше обновить такой массив в потоке пользовательского интерфейса.


Учитывая все вышесказанное, ваш код будет выглядеть примерно так:

func getAllActivities(userUID: String, _ completionHandler: @escaping (_ activities: [ActivitiesForUIStruct]?, _ error: Error?) -> Void) {
    let downloadGroup = DispatchGroup()
    let detailGroup = DispatchGroup()

    var activities = [ActivitiesForUIStruct]()

    getActivities(userUID: userUID) { (results, error) in
        //Handle error

        if let results = results {
            var groupTitle = ""
            var originatorName = ""
            var sourceName = ""

            results.forEach({ (activity) in
                //### 1. Balance enter() and leave()
                downloadGroup.enter()

                let originatorUID = activity.originatorUserUID ?? ""
                let groupID = activity.inspectionGroupID ?? ""
                let sourceID = activity.sourceID ?? ""

                //### 2. Guarantee all enter() executed before leave()
                detailGroup.enter() //for getTitle(key: "inspectionGroups"...
                detailGroup.enter() //for getName(userUID: originatorUID...
                detailGroup.enter() //for getTitle(key: "users"...

                self.getTitle(key: "inspectionGroups", value: groupID, { (title, error) in
                    //...
                    if let title = title {
                        groupTitle = title
                    }
                    detailGroup.leave()
                })

                self.getName(userUID: originatorUID, { (name, error) in
                    //...
                    if let name = name {
                        originatorName = name
                    }
                    detailGroup.leave()
                })

                self.getTitle(key: "users", value: sourceID, { (title, error) in
                    //...
                    if let title = title {
                        sourceName = title
                    }
                    detailGroup.leave()
                })

                //### 3. Use main queue to safely modify thread-unsafe objects
                detailGroup.notify(queue: .main, execute: {
                    let activityUI = ActivitiesForUIStruct(activities: activity, originatorName: originatorName, sourceName: sourceName, inspectionGroupTitle: groupTitle)
                    activities.append(activityUI)
                    downloadGroup.leave()
                })
            })
        }

        downloadGroup.notify(queue: .main, execute: {
            completionHandler(activities, nil)
        })
    }
}

В некоторых условиях некоторые из моих предложений могут не понадобиться, но вам лучше помнить все 3 (+1 предложено Code Different) при работе с DispatchGroup или асинхронных задачах.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...