Concurrent vs Serial DispatchQueue: объяснение параллелизма в Swift

Параллельные и последовательные очереди помогают нам управлять тем, как мы выполняем задачи, и помогают нашим приложениям работать быстрее, эффективнее и с улучшенным откликом. Мы можем легко создавать очереди, используя класс DispatchQueue, который построен поверх очереди Grand Central Dispatch (GCD).

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

Что такое DispatchQueue?

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

Что такое последовательная очередь?

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

Создание очереди последовательной отправки

DispatchQueue по умолчанию представляет собой последовательную очередь и может быть инициализирована следующим образом:

let serialQueue = DispatchQueue(label: "swiftlee.serial.queue")

serialQueue.async {
    print("Task 1 started")
    // Do some work..
    print("Task 1 finished")
}
serialQueue.async {
    print("Task 2 started")
    // Do some work..
    print("Task 2 finished")
}

/*
Serial Queue prints:
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished
*/

Как видите, вторая задача запускается только после завершения первой задачи.

Что такое параллельная очередь?

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

Создание параллельной очереди отправки

Параллельную очередь отправки можно создать, передав атрибут в качестве параметра инициализатору DispatchQueue:

let concurrentQueue = DispatchQueue(label: "swiftlee.concurrent.queue", attributes: .concurrent)

concurrentQueue.async {
    print("Task 1 started")
    // Do some work..
    print("Task 1 finished")
}
concurrentQueue.async {
    print("Task 2 started")
    // Do some work..
    print("Task 2 finished")
}

/*
Concurrent Queue prints:
Task 1 started
Task 2 started
Task 1 finished
Task 2 finished
*/

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

Лучший из двух миров

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

Что такое гонка данных?

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

Гонки данных могут быть основной причиной ненадежных тестов и странных сбоев. Поэтому рекомендуется регулярно проводить время с использованием Thread Sanitizer.

Использование барьера в параллельной очереди для синхронизации операций записи

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

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

final class Messenger {

    private var messages: [String] = []

    private var queue = DispatchQueue(label: "messages.queue", attributes: .concurrent)

    var lastMessage: String? {
        return queue.sync {
            messages.last
        }
    }

    func postMessage(_ newMessage: String) {
        queue.sync(flags: .barrier) {
            messages.append(newMessage)
        }
    }
}

let messenger = Messenger()
// Executed on Thread #1
messenger.postMessage("Hello SwiftLee!")
// Executed on Thread #2
print(messenger.lastMessage) // Prints: Hello SwiftLee!

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

Асинхронные и синхронные задачи

Задача DispatchQueue может выполняться синхронно или асинхронно. Основное отличие возникает при создании задачи.

  • Синхронный запуск задачи блокирует вызывающий поток до тех пор, пока задача не будет завершена.
  • Асинхронный запуск задачи будет напрямую возвращаться в вызывающий поток без блокировки

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

Как насчет основного потока?

Основная очередь отправки — это глобально доступная последовательная очередь, выполняющая задачи в основном потоке приложения. Поскольку основной поток используется для обновлений пользовательского интерфейса, важно быть внимательным при выполнении задач в этой очереди. Поэтому полезно использовать ранее описанные API-интерфейсы диспетчеризации для выполнения задач в другом потоке.

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

let concurrentQueue = DispatchQueue(label: "swiftlee.concurrent.queue", attributes: .concurrent)

concurrentQueue.async {
    // Perform the data request and JSON decoding on the background queue.
    fetchData()

    DispatchQueue.main.async {
        /// Access and reload the UI back on the main queue.
        tableView.reloadData()
    }
}

Избегайте чрезмерного создания потоков

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

Существует два распространенных сценария, в которых происходит чрезмерное создание потоков:

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

Как предотвратить чрезмерное создание потоков?

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

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

DispatchQueue.global().async {
    /// Concurrently execute a task using the global concurrent queue. Also known as the background queue.
}

Эта глобальная параллельная очередь также называется фоновой очередью и используется рядом с DispatchQueue.main.

Заключение

Вот и все, глубокое погружение в очереди отправки в Swift. Есть еще много чего, но основы описаны здесь. Обязательно ознакомьтесь с Thread Sanitizer, чтобы узнать, где вы можете улучшить свое приложение для борьбы с гонками данных.

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

Спасибо!