В Swift, если Thread.current.isMainThread == false, тогда безопасно ли один раз выполнять DispatchQueue.main.syn c? - PullRequest
1 голос
/ 02 мая 2020

В Swift, если Thread.current.isMainThread == false, тогда безопасно ли один раз выполнять DispatchQueue.main.syn c один раз?

Причина, по которой я спрашиваю, заключается в том, что в приложении моей компании у нас был cra sh, который оказался из-за того, что какой-то метод пользовательского интерфейса вызывался из основного потока, например:

public extension UIViewController {
    func presentModally(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        // some code that sets presentation style then:
        present(viewControllerToPresent, animated: flag, completion: completion)
    }
}

Так как его вызывали из многих мест, некоторые из которых иногда позвоните из фоновой ветки, тут и там были сбои.

Исправить все сайты вызовов было невозможно из-за того, что в приложении было более миллиона строк кода, поэтому я решил просто проверить, находимся ли мы в главном потоке, а если нет, то перенаправить вызов основного потока, например, так:

public extension UIViewController {
    func presentModally(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        guard Thread.current.isMainThread else {
            DispatchQueue.main.sync { 
                presentModally(viewControllerToPresent, animated: flag, completion: completion) 
            }
            return
        }

        // some code that sets presentation style then:
        present(viewControllerToPresent, animated: flag, completion: completion)
    }
}

Преимущества этого подхода кажутся следующими:

  • Сохранение порядка выполнения. Если Вызывающая сторона отключена от основного потока, мы перенаправим на основной поток, затем выполним ту же функцию, прежде чем вернуться - таким образом, сохраняя нормальный порядок выполнения, который был бы, если бы исходная функция была вызвана из основного потока, поскольку функции вызываемый в основном потоке (или любом другом потоке), по умолчанию выполняется синхронно.
  • Возможность неявно ссылаться на себя без предупреждений компилятора. В Xcode 11.4 выполнение этого вызова синхронно также удовлетворяет компилятору, который это нормально, чтобы неявно сохранить себя, так как контекст отправки будет введен, а затем выйти до начала Вызов функции inal возвращается - поэтому мы не получаем никаких новых предупреждений компилятора от этого подхода. Это красиво и чисто.
  • Больше сфокусированных различий за счет меньшего отступа. Это позволяет избежать оборачивания всего тела функции в замыкание (как вы обычно это делаете, если использовать Dispatch.main.async { ... }, где теперь все тело должно быть на более глубоком уровне, что приводит к появлению различий в пробелах в вашем PR, что может привести к раздражающим конфликтам слияний и усложнить рецензентам различение guish существенных элементов в представлениях различий PR в GitHub).

Между тем альтернатива, DispatchQueue.main.async, может иметь следующие недостатки:

  • Потенциально изменяет ожидаемый порядок выполнения. Функция вернется перед выполнением отправленное закрытие, которое, в свою очередь, означает, что self могло быть освобождено до его запуска. Это означает, что мы должны явно сохранять себя (или ослаблять его), чтобы избежать предупреждения компилятора. Это также означает, что в этом примере present(...) не будет вызвано до того, как функция вернется к вызывающей стороне. Это может привести к тому, что модал всплывет после некоторого другого кода, следующего за сайтом вызовов, что приведет к непреднамеренному поведению.
  • Требование либо ослабления, либо явного сохранения self. Это на самом деле не недостаток, но он не настолько чист, стилистически, как способность неявно сохранять себя.

Итак, вопрос в том, верны ли все эти предположения, или я что-то здесь упустил?

Мне показалось, что мои коллеги, которые просматривали PR, использовали DispatchQueue.main.syn c "как-то изначально плохо и рискованно и может привести к тупику. Хотя я понимаю, что использование этого из основного потока действительно приведет к взаимоблокировке, здесь мы явно избегаем этого, используя оператор guard, чтобы убедиться, что мы НЕ в основном потоке. 1048 все еще сохраняют глубокие сомнения относительно этого паттерна, полагая, что он может привести к тупику или неожиданному блокированию пользовательского интерфейса.

Основаны ли эти страхи? Или этот шаблон совершенно безопасен?

Ответы [ 2 ]

1 голос
/ 02 мая 2020

Эта модель определенно не «совершенно» безопасна. Можно легко придумать тупик:

let group = DispatchGroup()
DispatchQueue.global().async(group: group) {
    self.presentModally(controller, animated: true)
}
group.wait()

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

Но это не настоящая проблема. У вас, очевидно, есть какая-то подпрограмма, которая думает, что она работает в главном потоке, а когда нет. Лично я бы беспокоился о том, что еще этот код делал во время работы с этим заблуждением (например, несинхронизированные обновления моделей и т. Д. c.).

Ваш обходной путь, вместо того, чтобы устранить root причину проблемы, просто скрывает ее. Как правило, я бы не советовал кодировать ошибки, внесенные где-либо еще в кодовую базу. Вы действительно должны просто выяснить, откуда вы вызываете эту подпрограмму из фонового потока, и решить ее.


С точки зрения того, как найти проблему, мы надеемся, что трассировка стека, связанная с cra sh скажу тебе. Я бы также предложил добавить точку останова для средства проверки основного потока, нажав на эту маленькую стрелочку рядом с ней в настройках схемы:

Main thread breakpoint

Затем выполните упражнение приложение и, если оно столкнется с этой проблемой, оно приостановит выполнение на линии, вызывающей сбой, что может быть очень полезно для отслеживания этих проблем. Это часто намного проще, чем обратное проектирование из трассировки стека.

0 голосов
/ 02 мая 2020

Я согласен с комментариями о том, что у вас есть некоторые структурные трудности с вашим кодом.

Но все еще бывают моменты, когда мне нужен код для запуска в основном потоке, и я не знаю, являюсь ли я уже в основной ветке или нет. Это происходило достаточно часто, поэтому я написал ExecuteOnMain() функцию только для этого:

dispatch_queue_t MainSequentialQueue( )
{
    static dispatch_queue_t mainQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
#if HAS_MAIN_RUNLOOP
        // If this process has a main thread run loop, queue sequential tasks to run on the main thread
        mainQueue = dispatch_get_main_queue();
#else
        // If the process doesn't execute in a run loop, create a sequential utility thread to perform these tasks
        mainQueue = dispatch_queue_create("main-sequential",DISPATCH_QUEUE_SERIAL);
#endif
        });
    return mainQueue;
}

BOOL IsMainQueue( )
{
#if HAS_MAIN_RUNLOOP
    // Return YES if this code is already executing on the main thread
    return [NSThread isMainThread];
#else
    // Return YES if this code is already executing on the sequential queue, NO otherwise
    return ( MainSequentialQueue() == dispatch_get_current_queue() );
#endif
}

void DispatchOnMain( dispatch_block_t block )
{
    // Shorthand for asynchronously dispatching a block to execute on the main thread
    dispatch_async(MainSequentialQueue(),block);
}

void ExecuteOnMain( dispatch_block_t block )
{
    // Shorthand for synchronously executing a block on the main thread before returning.
    // Unlike dispatch_sync(), this won't deadlock if executed on the main thread.
    if (IsMainQueue())
        // If this is the main thread, execute the block immediately
        block();
    else
        // If this is not the main thread, queue the block to execute on the main queue and wait for it to finish
        dispatch_sync(MainSequentialQueue(),block);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...