Обработчик завершения вызывается дважды (с потоками)

В настоящее время я тестирую этот код на игровой площадке Xcode 10 (Swift 5):

func one() {
    let test = "bla"
    two(test, completion: { (returned) in
        print(returned)
        })
}

func two(_ test: String, completion: @escaping (_ returned: String?) -> Void) {
    DispatchQueue.global(qos:.background).async {
        if !test.isEmpty {
            //Some slow stuff
            DispatchQueue.main.async {
                return completion("hi!")

            }
        }

        //Also some slow stuff
        DispatchQueue.main.async {
            return completion(nil) //can't have this in "else"!
        }
    }
}

one()

Проблема в том, что печатаются и "привет", и "ноль".

Если я избавлюсь от многопоточности, он будет работать нормально, но с ним кажется, что он доберется до второго DispatchQueue.main.async до того, как у первого появится шанс вернуться.

В «Некоторые медленные вещи» if в моем реальном коде происходит гораздо больше вещей, но я не могу полагаться на то, что это займет достаточно много времени, чтобы вернуться до того, как будет вызван второй возврат.

Как мне это сделать: заставить функцию работать в фоновом потоке, но возвращать ее только один раз в основном потоке (как обычно делает код без потоков)?


person Neph    schedule 23.07.2019    source источник
comment
Какова ваша цель здесь? Вы хотите выполнить оба набора медленных вещей? Вы хотите только один вызов завершения?   -  person vacawama    schedule 23.07.2019
comment
Я хочу вызвать if и медленные вещи внутри, а затем вызвать обработчик завершения, чтобы вернуть результат обратно в one(). Если if не срабатывает, я хочу только вызвать также некоторые медленные вещи и вернуть nil.   -  person Neph    schedule 23.07.2019
comment
Проверьте мой ответ ниже. Я думаю, это делает то, что вы хотите. Мне любопытно, почему второй медленный материал не мог быть в else.   -  person vacawama    schedule 23.07.2019
comment
В этом тесте (на игровой площадке) это может быть else, но в моем реальном приложении это в основном длинный список if с единственным хорошим результатом. Конечно, я мог бы просто вернуться в каждом else, но мне также нужно сделать некоторые другие вещи, прежде чем я это сделаю (например, закрыть сокеты), и тогда тот же код будет там примерно. 10 раз.   -  person Neph    schedule 23.07.2019


Ответы (3)


Я считаю, что ваша цель состоит в том, чтобы вызвать обработчик completion только один раз, и когда вы это сделаете, все будет готово. В этом случае вызовите return в потоке .background после постановки в очередь вызова завершения в основном потоке:

func two(_ test: String, completion: @escaping (_ returned: String?) -> Void) {
    DispatchQueue.global(qos:.background).async {
        if !test.isEmpty {
            //Some slow stuff

            // notify main thread we're done
            DispatchQueue.main.async {
                completion("hi!")
            }

            // we are done and don't want to do more work on the
            // background thread
            return
        }

        //Also some slow stuff
        DispatchQueue.main.async {
            completion(nil)
        }
    }
}
person vacawama    schedule 23.07.2019
comment
Черт, это действительно так просто, и порядок, в котором все вызывается, тоже правильный. Спасибо! Просто интересно: Xcode не жалуется на return completion("hi"), но его удаление, похоже, не имеет большого значения, так что же именно делает return в сочетании с обработчиком завершения? Это больше похоже на то, что if somebool == true совпадает с if somebool? - person Neph; 23.07.2019
comment
return completion("hi!") вызовет обработчик завершения и вернет результат (). Поскольку замыкание все равно возвращается, return бесполезен. - person vacawama; 23.07.2019
comment
Я только что проверил его с моим реальным кодом за пределами игровой площадки, и там он тоже отлично работает, так что еще раз спасибо! - person Neph; 23.07.2019

Почему это называется дважды, это довольно очевидно. (кстати, вы не возвращаете блоки завершения). Вы написали следующее:

func two(_ test: String, completion: @escaping (_ returned: String?) -> Void) {
    DispatchQueue.global(qos:.background).async {
        if !test.isEmpty {
            //Some slow stuff
            DispatchQueue.main.async {
                completion("hi!")

            }
        }
        completion(nil) //can't have this in "else"!
    }
}

А поскольку основной поток всегда продолжает работать, пока вы занимаетесь другими делами в фоновой угрозе, вы получите 2 завершения. Что вы хотите сделать, я думаю, это удалить второй?

Альтернативой является то, что вы создаете DispatchGroup и вводите каждый вызов внутри него, а затем пишете Dispatch wait till done, чтобы дождаться завершения всех запросов.

person Vollan    schedule 23.07.2019
comment
What you want to do i guess is to remove the second one? - Нет, второй DispatchQueue.main.async тоже нужен, иначе completion(nil) вернётся в фоновом потоке. Мне придется взглянуть на DispatchGroup, но если есть способ сделать это только с DispatchQueue, я бы предпочел это. - person Neph; 23.07.2019

Вы можете использовать оператор defer для возврата блока завершения один раз после всех операторов If. Вот только пример с вашим кодом, но надеюсь понятно.

func two(_ test: String, completion: @escaping (_ returned: String?) -> Void) {

        DispatchQueue.global(qos:.background).async {

            var resultString: String? 

            // Called only once after all code inside this async block.
            defer {
                DispatchQueue.main.async {
                    completion(resultString)
                }
            }

            if !test.isEmpty {

               //Some slow stuff
               resultString = "hi"

               return 
            }

            // Another stuff 
            resultString = nil
        }
    }
person Ildar.Z    schedule 23.07.2019
comment
Как указано в вопросе, я не могу также делать некоторые медленные вещи в else. Таким образом, хотя defer хорошо использовать в других случаях, здесь он не сработает, потому что, если я удалю else рядом с некоторыми медленными вещами, это просто перезапишет результат. - person Neph; 23.07.2019
comment
Я понимаю это, но перезапись результата зависит от ваших операторов if, поэтому вы можете написать операторы, которые не будут перезаписывать результат. - person Ildar.Z; 23.07.2019
comment
Вы можете быть уверены, что он не будет перезаписан, если вы используете else, чего я не могу из-за того, как мой фактический код настроен/работает. Удаление else и просто наличие resultString = nil там голым всегда будет возвращать nil. - person Neph; 23.07.2019
comment
Да, но в любом случае вы можете смешать это с возвратом, код будет более понятным только с одним возвратом блока завершения. Обновленный ответ. - person Ildar.Z; 23.07.2019