Параллельные и последовательные очереди в GCD

Я пытаюсь полностью понять параллельные и последовательные очереди в GCD. У меня есть некоторые проблемы, и я надеюсь, что кто-то сможет ответить мне ясно и на месте.

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

    • Я создаю серийную очередь
    • Я использую dispatch_async (в последовательной очереди, которую я только что создал) три раза, чтобы отправить три блока A, B, C

    Будут выполняться три блока:

    • в порядке A, B, C, поскольку очередь является последовательной

      ИЛИ

    • одновременно (в то же время на потоках parralel), потому что я использовал отправку ASYNC
  • Я читаю, что я могу использовать dispatch_sync в параллельных очередях для выполнения блоков один за другим. В этом случае ПОЧЕМУ серийные очереди существуют, так как я всегда могу использовать параллельную очередь, где я могу отправить SYNCHRONOUSLY столько блоков, сколько захочу?

    Спасибо за хорошее объяснение!

Ответ 1

Простой пример: у вас есть блок, для выполнения которого требуется минута. Вы добавляете его в очередь из основного потока. Давайте рассмотрим четыре случая.

  • async - concurrent: код работает в фоновом потоке. Элемент управления немедленно возвращается к основному потоку (и пользовательскому интерфейсу). Блок не может предположить, что он единственный блок, работающий в этой очереди
  • async - serial: код запускается в фоновом потоке. Элемент управления немедленно возвращается к основному потоку. Блок может предположить, что это единственный блок, запущенный в этой очереди
  • sync - одновременный: код работает в фоновом потоке, но основной поток ждет его завершения, блокируя любые обновления для пользовательского интерфейса. Блок не может предположить, что он является единственным блоком, запущенным в этой очереди (я мог бы добавить еще один блок с использованием async за несколько секунд до этого)
  • sync - serial: код запускается в фоновом потоке, но основной поток ждет его завершения, блокируя любые обновления для пользовательского интерфейса. Блок может предположить, что это единственный блок, запущенный в этой очереди

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

Ответ 2

Вот несколько экспериментов, которые я сделал, чтобы понять, о чем эти очереди serial, concurrent с Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

   let serialQueue = DispatchQueue(label: "com.queue.Serial")
      for i in 1...5 {
        serialQueue.async {

            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Задание будет выполняться в другом потоке (кроме основного потока) при использовании async в GCD. Async означает выполнение следующей строки, не дожидаясь, пока блок выполнит какие результаты не будут блокировать основной поток и главную очередь.     Начиная с его последовательной очереди все выполняется в том порядке, в котором они добавляются в последовательную очередь. Задания, выполняемые последовательно, всегда выполняются по одному отдельным потоком, связанным с очередью.

func doLongSyncTaskInSerialQueue() {
    let serialQueue = DispatchQueue(label: "com.queue.Serial")
    for i in 1...5 {
        serialQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Задача может выполняться в основном потоке при использовании синхронизации в GCD. Синхронизация запускает блок в заданной очереди и ждет, пока он завершит выполнение, что приведет к блокировке основного потока или основной очереди. Поскольку главная очередь должна дождаться завершения отправленного блока, основной поток будет доступен для обработки блоков из очередей, отличных от главной очереди. Поэтому существует вероятность того, что выполнение кода в фоновом режиме может фактически выполняться в основном потоке     С его последовательной очереди все выполняется в том порядке, в котором они добавлены (FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executing")
    }
}

Задание будет выполняться в фоновом потоке при использовании async в GCD. Async означает выполнение следующей строки, не дожидаясь, пока блок выполнит, какие результаты не будут блокировать основной поток.     Помните, что в параллельной очереди задача обрабатывается в том порядке, в котором они добавляются в очередь, но с разными потоками, прикрепленными к очередь. Помните, что они не должны заканчивать задачу как заказ они добавляются в очередь. Каждый из задач различается каждый раз потоки создаются как обязательно автоматически. Задача выполняется параллельно. С более чем что (maxConcurrentOperationCount) достигнуто, некоторые задачи будут вести себя как серийный, пока поток не станет свободным.

func doLongSyncTaskInConcurrentQueue() {
  let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executed")
    }
}

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

Вот краткое изложение этих экспериментов

Помните, что с помощью GCD вы добавляете задачу в очередь и выполняете задачу из этой очереди. Очередь отправляет вашу задачу в основном или фоновом потоке в зависимости от того, является ли операция синхронной или асинхронной. Типы очередей - это последовательная, параллельная, основная диспетчерская очередь. Вся выполняемая вами задача выполняется по умолчанию из главной диспетчерской очереди. Уже существует четыре предопределенные глобальные параллельные очереди для вашего приложения и одна главная очередь (DispatchQueue.main). также может вручную создать свою собственную очередь и выполнить задачу из этой очереди.

UI Связанная задача всегда должна выполняться из основного потока путем отправки задачи в главную очередь. Командная утилита для командной строки DispatchQueue.main.sync/async, тогда как сетевые/тяжелые операции всегда должны выполняться асинхронно, без каких-либо вопросов, которые когда-либо используются, вы используете либо основной, либо фон

EDIT: Тем не менее, есть случаи, когда вам нужно выполнять операции сетевых вызовов синхронно в фоновом потоке без замораживания UI (например, восстанавливать токен OAuth и ждать, удастся ли это или нет). Вам необходимо обернуть этот метод внутри асинхронной операции. Таким образом, ваши тяжелые операции выполняются в порядке и без основной блокировки.

func doMultipleSyncTaskWithinAsynchronousOperation() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    concurrentQueue.async {
        let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        for i in 1...5 {
            concurrentQueue.sync {
                let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
                let _ = try! Data(contentsOf: imgURL)
                print("\(i) completed downloading")
            }
            print("\(i) executed")
        }
    }
}

EDIT EDIT: Вы можете посмотреть демонстрационное видео здесь

Ответ 3

Во-первых, важно знать разницу между потоками и очередями и тем, что на самом деле делает GCD. Когда мы используем очереди рассылки (через GCD), мы действительно в очереди, а не в потоке. Платформа Dispatch была разработана специально для того, чтобы избавить нас от многопоточности, так как Apple признает, что "внедрение правильного решения для многопоточности [может] стать чрезвычайно трудным, а иногда даже невозможным". Поэтому для одновременного выполнения задач (задач, которые нам не нужны, чтобы заморозить интерфейс) все, что нам нужно сделать, - это создать очередь этих задач и передать ее в GCD. И GCD обрабатывает все связанные потоки. Поэтому все, что мы на самом деле делаем, это очереди.

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

serialQueue.async {
    // this is one task
    // it can be any number of lines with any number of methods
}
serialQueue.async {
    // this is another task added to the same queue
    // this queue now has two tasks
}

Существует два типа очередей: последовательные и параллельные, но все очереди параллельны друг другу. Тот факт, что вы хотите запускать любой код "в фоновом режиме", означает, что вы хотите запускать его одновременно с другим потоком (обычно основным потоком). Следовательно, все очереди отправки, последовательные или параллельные, выполняют свои задачи одновременно по отношению к другим очередям. Любая сериализация, выполняемая очередями (последовательными очередями), имеет отношение только к задачам в этой единственной [последовательной] очереди отправки (как в примере выше, где есть две задачи в одной последовательной очереди; эти задачи будут выполняться одна после другой, никогда одновременно).

Последовательные очереди (часто известные как частные очереди отправки) гарантируют выполнение задач по одному от начала до конца в порядке их добавления в эту конкретную очередь. Это единственная гарантия сериализации в любом месте обсуждения очередей отправки - то, что конкретные задачи в определенной последовательной очереди выполняются последовательно. Последовательные очереди могут, однако, выполняться одновременно с другими последовательными очередями, если они являются отдельными очередями, потому что, опять же, все очереди параллельны друг другу. Все задачи выполняются в разных потоках, но не каждая задача гарантированно выполняется в том же потоке (не важно, но интересно знать). И фреймворк iOS не поставляется с готовыми к использованию последовательными очередями, их нужно создавать. Частные (неглобальные) очереди по умолчанию являются последовательными, поэтому для создания последовательной очереди:

let serialQueue = DispatchQueue(label: "serial")

Вы можете сделать это одновременно через свойство атрибута:

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])

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

Параллельные очереди (часто известные как глобальные очереди отправки) могут выполнять задачи одновременно; однако задачи гарантированно инициируются в том порядке, в котором они были добавлены в эту конкретную очередь, но, в отличие от последовательных очередей, очередь не ожидает завершения первой задачи, прежде чем запустить вторую задачу. Задачи (как в случае последовательных очередей) выполняются в разных потоках и (как в случае последовательных очередей) гарантированно не все задачи выполняются в одном и том же потоке (что не важно, но интересно знать). И платформа iOS поставляется с четырьмя готовыми к использованию параллельными очередями. Вы можете создать параллельную очередь, используя приведенный выше пример, или просто использовать одну из глобальных очередей Apple (что обычно рекомендуется):

let concurrentQueue = DispatchQueue.global(qos: .default)

RETAIN-CYCLE RESISTANT: очереди отправки являются объектами с подсчетом ссылок, но вам не нужно сохранять и освобождать глобальные очереди, потому что они являются глобальными, и, следовательно, сохранение и освобождение игнорируются. Вы можете получить доступ к глобальным очередям напрямую, не назначая их свойству.

Существует два способа отправки очередей: синхронно и асинхронно.

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

DispatchQueue.global(qos: .default).sync {
    // task goes in here
}

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

DispatchQueue.global(qos: .default).async {
    // task goes in here
}

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

whichQueueShouldIUse.syncOrAsync {

    for i in 1...10 {
        print(i)
    }
    for i in 1...10 {
        print(i + 100)
    }
    for i in 1...10 {
        print(i + 1000)
    }

}

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

Рассмотрим эти две очереди, одну последовательную и одну параллельную:

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)

Скажем, мы отправляем две параллельные очереди в асинхронном режиме:

concurrentQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
103
3
104
4
105
5

Их выходные данные перемешаны (как и ожидалось), но обратите внимание, что каждая очередь выполняет свою собственную задачу последовательно. Это самый простой пример параллелизма - две задачи, выполняющиеся одновременно в фоновом режиме, в одной очереди. Теперь давайте сделаем первый сериал:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

101
1
2
102
3
103
4
104
5
105

Разве первая очередь не должна выполняться последовательно? Это было (и так было вторым). Что бы ни случилось на заднем плане, это не имеет никакого отношения к очереди. Мы приказали, чтобы последовательная очередь выполнялась последовательно, и она выполнила... но мы дали ей только одну задачу. Теперь давайте дадим ему две задачи:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

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

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue2.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
3
103
4
104
5
105

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

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 1000)
    }
}

1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005

Это неожиданно, почему параллельная очередь дожидалась окончания последовательных очередей, прежде чем она запустилась? Это не параллелизм. Ваша игровая площадка может показывать другой результат, но моя показала это. И это показало это, потому что мой приоритет параллельной очереди был недостаточно высок для GCD, чтобы выполнить свою задачу быстрее. Поэтому, если я сохраню все то же самое, но изменю глобальное QoS очереди (его качество обслуживания, которое является просто уровнем приоритета очереди), let concurrentQueue = DispatchQueue.global(qos:.userInteractive), тогда вывод будет таким, как ожидается:

1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105

Две последовательные очереди выполняли свои задачи последовательно (как и ожидалось), а параллельная очередь выполняла свою задачу быстрее, поскольку ей был присвоен высокий уровень приоритета (высокое качество обслуживания или качество обслуживания).

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

concurrentQueue.sync {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

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

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1

Ответ 4

Если я правильно понимаю, как работает GCD, я думаю, что есть два типа DispatchQueue, serial и concurrent, в то же время есть два способа, как DispatchQueue отправлять свои задачи, назначенные closure, первый - async, а другой - sync. Они вместе определяют, как выполняется выполнение (задача).

Я обнаружил, что serial и concurrent означает, сколько потоков, которые может использовать очередь, serial означает один, тогда как concurrent означает много. И sync и async означают, что задача будет выполняться в том потоке, потоке вызывающего или потоке, лежащем в основе этой очереди, sync означает запуск в потоке вызывающего абонента, тогда как async означает выполнение в нижнем потоке.

Ниже приведен экспериментальный код, который может работать на игровой площадке Xcode.

PlaygroundPage.current.needsIndefiniteExecution = true
let cq = DispatchQueue(label: "concurrent.queue", attributes: .concurrent)
let cq2 = DispatchQueue(label: "concurent.queue2", attributes: .concurrent)
let sq = DispatchQueue(label: "serial.queue")

func codeFragment() {
  print("code Fragment begin")
  print("Task Thread:\(Thread.current.description)")
  let imgURL = URL(string: "http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground")!
  let _ = try! Data(contentsOf: imgURL)
  print("code Fragment completed")
}

func serialQueueSync() { sq.sync { codeFragment() } }
func serialQueueAsync() { sq.async { codeFragment() } }
func concurrentQueueSync() { cq2.sync { codeFragment() } }
func concurrentQueueAsync() { cq2.async { codeFragment() } }

func tasksExecution() {
  (1...5).forEach { (_) in
    /// Using an concurrent queue to simulate concurent task executions.
    cq.async {
      print("Caller Thread:\(Thread.current.description)")
      /// Serial Queue Async, tasks run serially, because only one thread that can be used by serial queue, the underlying thread of serial queue.
      //serialQueueAsync()
      /// Serial Queue Sync, tasks run serially, because only one thread that can be used by serial queue,one by one of the callers' threads.
      //serialQueueSync()
      /// Concurrent Queue Async, tasks run concurrently, because tasks can run on different underlying threads
      //concurrentQueueAsync()
      /// Concurrent Queue Sync, tasks run concurrently, because tasks can run on different callers' thread
      //concurrentQueueSync()
    }
  }
}
tasksExecution()

Надеюсь, это будет полезно.

Ответ 5

1. Я читаю, что последовательные очереди создаются и используются для выполнения задач одна за другой. Однако, что произойдет, если: - • я создаю последовательную очередь • я использую dispatch_async (в только что созданной последовательной очереди) три раза для отправки трех блоков A, B, C

ОТВЕТ: - Все три блока выполняются один за другим. Я создал один пример кода, который помогает понять.

let serialQueue = DispatchQueue(label: "SampleSerialQueue")
//Block first
serialQueue.async {
    for i in 1...10{
        print("Serial - First operation",i)
    }
}

//Block second
serialQueue.async {
    for i in 1...10{
        print("Serial - Second operation",i)
    }
}
//Block Third
serialQueue.async {
    for i in 1...10{
        print("Serial - Third operation",i)
    }
}

Ответ 6

Мне нравится думать, используя эту метафору (здесь ссылка на исходное изображение):

Dad's gonna need some help

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

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

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

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

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

Надеюсь это поможет