Поведение sync / async аналогично последовательному / параллельному, т.е. контролируют ли они оба DispatchQueues или синхронизируют / асинхронно контролируют только потоки

Большинство ответов на stackoverflow подразумевают, что поведение синхронизации и асинхронности очень похоже на разницу в концепции последовательной и параллельной очереди. Как ссылка в первом комментарии @Roope

Я начал думать, что Serial и concurrent связаны с DispatchQueue и sync / async для того, как операция будет выполняться в потоке. Я прав?

Например, если у нас есть DQ.main.sync, то закрытие задачи / операции будет выполняться синхронно в этой последовательной (основной) очереди. И, если я сделаю DQ.main.async, тогда задача будет асинхронно работать в какой-то другой фоновой очереди, и по достижении завершения вернет управление в основном потоке. И, поскольку main - это последовательная очередь, она не позволит какой-либо другой задаче / операции перейти в состояние выполнения / начать выполнение до тех пор, пока текущая задача закрытия не завершит свое выполнение.

Затем DQ.global().sync будет выполнять задачу синхронно в потоке, которому назначена его задача / операция, то есть он будет блокировать этот поток от выполнения любой другой задачи / операции, блокируя любое переключение контекста в этом конкретном потоке. И поскольку global является параллельной очередью, она будет продолжать переводить присутствующие в ней задачи в состояние выполнения независимо от состояния выполнения предыдущей задачи / операции.

DQ.global().async позволит переключать контекст в потоке, в котором закрытие операции было помещено для выполнения

Это правильная интерпретация приведенных выше dispatchQueues и sync vs async?


person Priyank    schedule 17.11.2019    source источник
comment
@Roope Приведенная выше ссылка также не имеет полной разницы и кажется довольно неоднозначной. Он говорит: Синхронная функция возвращает управление текущей очереди только после завершения задачи. Он блокирует очередь и ждет, пока задача не будет завершена. Асинхронная функция возвращает управление текущей очереди сразу после того, как задача была отправлена ​​для выполнения в другой очереди. Он не дожидается завершения задачи. Не блокирует очередь. В приведенном выше тексте синхронизация и асинхронность фактически очень похожа на разницу между последовательными и параллельными очередями.   -  person Priyank    schedule 18.11.2019
comment
Я не совсем понимаю, что вы имеете в виду, ответы на вопрос, который я связал, недвусмысленны. Но давайте посмотрим, вы скажете, что если я сделаю DQ.main.async, тогда задача будет асинхронно работать в какой-то другой фоновой очереди, и по достижении завершения вернет управление в основном потоке. Почему вы считаете, что вызов async в основной очереди приведет к тому, что этот фрагмент кода будет выполнен в некоторой другой очереди, чем основная очередь? В какой бы очереди вы ни вызывали async / sync, код будет выполнен. Единственная разница в том, будет ли ваша текущая очередь ждать результата или нет.   -  person Roope    schedule 18.11.2019


Ответы (2)


Я начал думать, что Serial и concurrent связаны с DispatchQueue и sync / async для того, как операция будет выполняться в потоке.

Да, выбор последовательной или параллельной очереди определяет поведение той очереди, в которую вы отправляете, но _1 _ / _ 2_ не имеет ничего общего с тем, как этот код работает в этой другой очереди. Вместо этого он определяет поведение потока, из которого вы отправили. Итак, вкратце:

  • Независимо от того, является ли очередь назначения последовательной или параллельной, определяется, как эта очередь назначения будет вести себя (а именно, может ли эта очередь запускать это закрытие одновременно с другими вещами, которые были отправлены в ту же очередь, или нет);

  • В то время как sync vs async диктует, как текущий поток, из которого вы отправляете, будет вести себя (а именно, должен ли вызывающий поток ждать завершения отправленного кода или нет).

Таким образом, последовательный / параллельный режим влияет на очередь назначения в, которую вы отправляете, тогда как _5 _ / _ 6_ влияет на текущий поток из, который вы отправляете.

Вы продолжаете говорить:

Например, если у нас есть DQ.main.sync, то закрытие задачи / операции будет выполняться синхронно в этой последовательной (основной) очереди.

Я мог бы перефразировать это так: «если у нас есть DQ.main.sync, то текущий поток будет ждать, пока основная очередь выполнит это закрытие».

Помните, что «синхронный способ» не имеет ничего общего с тем, что происходит в целевой очереди (основная очередь в вашем DQ.main.sync примере), а скорее с потоком, из которого вы вызвали sync. Текущий поток будет ждать или нет?

FWIW, мы не используем DQ.main.sync очень часто, потому что в 9 случаях из 10 мы делаем это просто для отправки некоторого обновления пользовательского интерфейса, и обычно нет необходимости ждать. Это мелочь, но мы почти всегда используем DQ.main.async. Мы действительно используем sync, когда пытаемся обеспечить поточно-ориентированное взаимодействие с каким-либо ресурсом. В этом случае sync может быть очень полезным. Но часто это не требуется в сочетании с main, а только снижает эффективность.

И, если я сделаю DQ.main.async, тогда задача будет асинхронно работать в какой-то другой фоновой очереди, и по достижении завершения вернет управление в основном потоке.

No.

Когда вы делаете DQ.main.async, вы указываете, что закрытие будет выполняться асинхронно в основной очереди (очереди, в которую вы отправили), и что ваш текущий поток (предположительно фоновый поток) не должен ждать его, но будет немедленно продолжать.

Например, рассмотрим пример сетевого запроса, ответы на который обрабатываются в фоновой последовательной очереди URLSession:

let task = URLSession.shared.dataTask(with: url) { data, _, error in
    // parse the response
    DispatchQueue.main.async { 
        // update the UI
    }
    // do something else
}
task.resume()

Итак, синтаксический анализ происходит в этом URLSession фоновом потоке, он отправляет обновление пользовательского интерфейса в основной поток, а затем продолжает делать что-то еще в этом фоновом потоке. Вся цель sync vs async состоит в том, должно ли «делать что-то еще» ждать завершения «обновления пользовательского интерфейса» или нет. В этом случае нет смысла блокировать текущий фоновый поток, пока основной обрабатывает обновление пользовательского интерфейса, поэтому мы используем async.

Затем DQ.global().sync будет выполнять задачу синхронно в потоке, которому назначена ее задача / операция, т. Е. ...

Да DQ.global().sync говорит: «Запустите это закрытие в фоновой очереди, но заблокируйте текущий поток, пока это закрытие не будет выполнено».

Излишне говорить, что на практике мы никогда бы не сделали DQ.global().sync. Нет смысла блокировать текущий поток, ожидая выполнения чего-либо в глобальной очереди. Весь смысл диспетчеризации закрытий в глобальные очереди состоит в том, чтобы вы не блокировали текущий поток. Если вы рассматриваете DQ.global().sync, вы можете просто запустить его в текущем потоке, потому что вы все равно его блокируете. (Фактически, GCD знает, что DQ.global().sync ничего не добьется и, в качестве оптимизации, все равно будет запускать это в текущем потоке.)

Теперь, если по какой-то причине вы собирались использовать async или какую-то настраиваемую очередь, это может иметь смысл. Но вообще нет смысла делать DQ.global().sync.

... он заблокирует этот поток от выполнения любой другой задачи / операции, заблокировав любое переключение контекста в этом конкретном потоке.

No.

sync не влияет на «этот поток» (рабочий поток глобальной очереди). sync влияет на текущий поток, из которого вы отправили этот блок кода. Будет ли этот текущий поток ждать, пока глобальная очередь выполнит отправленный код (sync) или нет (async)?

И, поскольку global является параллельной очередью, она будет продолжать переводить присутствующие в ней задачи в состояние выполнения независимо от состояния выполнения предыдущей задачи / операции.

да. Опять же, я мог бы перефразировать это: «И поскольку global - текущая очередь, это закрытие будет запланировано на немедленное выполнение, независимо от того, что уже может быть запущено в этой очереди».

Техническое отличие состоит в том, что когда вы отправляете что-то в параллельную очередь, хотя обычно это начинается немедленно, иногда этого не происходит. Возможно, все ядра вашего процессора связаны с чем-то другим. Или, возможно, вы отправили много блоков и временно исчерпали очень ограниченное количество «рабочих потоков» GCD. В итоге, хотя обычно он запускается немедленно, всегда могут быть ограничения ресурсов, которые мешают этому.

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

DQ.global().async разрешит переключение контекста в потоке, в котором закрытие операции было помещено для выполнения.

Я мог бы избежать фразы «переключение контекста», поскольку она имеет очень конкретное значение, которое, вероятно, выходит за рамки этого вопроса. Если вам действительно интересно, вы можете посмотреть видео WWDC 2017 Modernizing Grand Central Dispatch Usage.

Я бы описал DQ.global().async следующим образом: он просто «позволяет текущему потоку продолжить работу без блокировки, в то время как глобальная очередь выполняет отправленное закрытие». Это чрезвычайно распространенный метод, который часто вызывается из основной очереди для отправки некоторого вычислительно интенсивного кода в некоторую глобальную очередь, но не дожидаясь его завершения, оставляя основной поток свободным для обработки событий пользовательского интерфейса, что приводит к более отзывчивому пользовательскому интерфейсу.

person Rob    schedule 30.12.2019
comment
Отличное объяснение - person Let's_Create; 30.12.2019

Вы задаете правильные вопросы, но я думаю, что вы немного запутались (в основном из-за не очень четких сообщений по этой теме в Интернете).

Параллельный / последовательный

Давайте посмотрим, как можно создать новую очередь отправки:

let serialQueue = DispatchQueue(label: label)

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

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


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

let concurrentQueue = DispatchQueue(label: label,
                      qos: .background,
                      attributes: .concurrent,
                      autoreleaseFrequency: .inherit,
                      target: .global())

Итак, вам просто нужно передать атрибут concurrent в очередь, и он больше не будет последовательным.

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


Если вы хотите больше узнать о параллельных очередях, (иначе: пропустите, если вас не интересуют параллельные очереди)

Вы можете спросить: когда мне вообще нужна параллельная очередь?

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

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

concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })

Другими словами, вы можете временно заставить параллельную очередь работать как последовательную очередь, если вам это нужно.


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

СИНХРОНИЗАЦИЯ / АСИНХРОНИЗАЦИЯ

Это совершенно другая проблема, практически не имеющая отношения к предыдущей.

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

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

func someMethod() {
    var aString = "1"
    DispatchQueue.main.async {
        aString = "2"
    }
    print(aString)
}

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

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

Если вместо этого вы отправите его синхронизацию, вы ВСЕГДА напечатали бы 2 вместо 1, потому что текущая очередь ожидала бы завершения этого блока кода, прежде чем продолжить его выполнение.

Итак, это напечатает 2:

func someMethod() {
    var aString = "1"
    DispatchQueue.main.sync {
        aString = "2"
    }
    print(aString)
}

Но означает ли это, что очередь, в которой вызывается someMethod, фактически остановлена?

Ну, это зависит от текущей очереди:

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

Что произойдет, если currentQueue и очередь, в которую мы отправляем, совпадают?

Предполагая, что мы находимся в последовательных очередях (что, я думаю, будет в большинстве ваших вариантов использования)

  • Если мы отправляем синхронизацию, то тупик. В этой очереди больше ничего не будет выполняться. Это худшее, что могло случиться.
  • В случае отправки async код будет выполнен в конце всего кода, уже отправленного в эту очередь (включая, помимо прочего, код, выполняющийся прямо сейчас в someMethod)

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

Надеюсь, это поможет вам лучше понять.

person Enricoza    schedule 18.11.2019
comment
Стоит отметить, что вызов sync в текущей очереди приведет к взаимной блокировке очереди. - person Roope; 18.11.2019
comment
@Roope Я думал, что это немного выходит за рамки, но просто чтобы избежать тупиковых ситуаций у некоторых людей, просто случайно читающих мой комментарий и предполагая, что это все, что вам нужно знать об этом, я тоже добавил это. Спасибо. - person Enricoza; 18.11.2019