Здравствуйте, уважаемые читатели Хабра!

Меня зовут Кирилл, я iOS-разработчик приложений Сбера в Студии Олега Чулакова.

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

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

В этой статье мы разберемся с основами многопоточности и познакомимся с такими инструментами Swift, как Grand Central Dispatch (GCD), OperationQueues, NSLock. А также существующими низкоуровневыми технологиями, такими как pthread и NSThread. Бонусом я расскажу про async/await. Мы обсудим, как эти инструменты помогают управлять асинхронными и параллельными операциями, и посмотрим на некоторые примеры их использования.

Приятного чтения, и давайте начнем наше путешествие в мир многопоточности Swift!

Основы многопоточности

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

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

Многопоточность в Swift

Swift предлагает различные методы реализации многопоточности, такие как Grand Central Dispatch (GCD), OperationQueue, NSLock. И низкоуровневые технологии, такие как pthread и NSThread.

Использование pthread и NSThread

pthread

POSIX Threads (pthread) — это стандартный интерфейс для работы с потоками в операционных системах UNIX. pthread предоставляет набор функций и типов данных, которые позволяют создавать и управлять потоками выполнения. Хотя pthread — это низкоуровневый интерфейс, он предлагает гибкость и полный контроль над созданием и управлением потоками.

Для создания нового потока с использованием pthread в Swift, вам необходимо выполнить следующие шаги:

import Foundation

var thread: pthread_t?

func startThread() {
    pthread_create(&thread, nil, threadFunction, nil)
}

func threadFunction(arg: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
    // Код, выполняемый в новом потоке
    return nil
}

func joinThread() {
    pthread_join(thread!, nil)
}

Данный код демонстрирует создание нового потока с помощью pthread_create(). Вы также можете определить свою функцию потока, которая будет выполняться в новом потоке. После создания потока вы можете присоединиться к нему с помощью pthread_join(), чтобы дождаться его завершения.

NSThread

NSThread — это класс Objective-C, который предоставляет более высокоуровневый интерфейс для работы с потоками в iOS и macOS. NSThread основан на pthread и предоставляет удобные методы для создания, управления и синхронизации потоков.

Пример использования NSThread:

import Foundation

var thread: Thread?

func startThread() {
    thread = Thread(target: self, selector: #selector(threadFunction), object: nil)
    thread?.start()
}

@objc func threadFunction() {
    // Код, выполняемый в новом потоке
}

func joinThread() {
    thread?.cancel()
    thread = nil
}

В этом примере мы используем класс Thread для создания нового потока и указываем целевой объект (self) и селектор функции потока (#selector(threadFunction)). Затем мы запускаем поток с помощью метода start(). Для остановки потока мы вызываем метод cancel() и устанавливаем ссылку на поток в nil.

Выбор между pthread и NSThread

При выборе между pthread и NSThread важно учитывать контекст вашего проекта и требования к многопоточности. pthread обеспечивает более низкоуровневый доступ и большую гибкость, но требует большего количества кода для создания и управления потоками. NSThread, с другой стороны, предлагает более высокоуровневый интерфейс и интеграцию с Objective-C, что делает его более удобным для использования.

Однако стоит отметить, что начиная с iOS 10 и macOS 10.12, Apple рекомендует использовать более современные и высокоуровневые подходы к многопоточности, такие как Grand Central Dispatch (GCD) и Operation Queues. Эти подходы предоставляют более удобный и безопасный способ работы с многопоточностью, особенно в контексте асинхронных операций.

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

Grand Central Dispatch в Swift: углубленный обзор с примерами

Grand Central Dispatch (GCD) — это низкоуровневая библиотека, разработанная Apple, позволяющая управлять параллельными задачами в многопоточном окружении. В центре концепции GCD лежат очереди задач и их выполнение в различных потоках.

GCD использует два типа очередей: серийные и параллельные.

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

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

Давайте разберемся с основами и рассмотрим несколько примеров.

Очереди выполнения

Очереди выполнения в GCD — это места, куда вы помещаете задачи для выполнения. В Swift существуют три типа очередей.

1. Основная очередь (Main Queue). Выполняется в основном потоке и используется для обновления пользовательского интерфейса. Это последовательная очередь.

let mainQueue = DispatchQueue.main

2. Глобальная очередь (Global Queue). Это системная предоставленная очередь, которая выполняется в фоновом режиме. Это параллельная очередь, и она бывает разных уровней приоритета.

let globalQueue = DispatchQueue.global()

3. Пользовательская очередь (Custom Queue). Программист может создавать собственные очереди. Эти очереди могут быть последовательными или параллельными.

let customQueue = DispatchQueue(label: "com.example.queue")

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

GCD выполняет задачи асинхронно или синхронно.

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

globalQueue.async {
    print("Выполняем асинхронную задачу")
}

2. Синхронные задачи не возвращают управление, пока задача не завершена.

globalQueue.sync {
    print("Выполняем синхронную задачу")
}

Обновление пользовательского интерфейса

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

DispatchQueue.global().async {
    // Фоновая задача
    let result = performHeavyTask()

    DispatchQueue.main.async {
        // Обновляем UI в основном потоке
        updateUIWithResult(result)
    }
}

Выполнение задачи после задержки

GCD позволяет запланировать выполнение задачи с задержкой.

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
    // Ваш код здесь будет выполнен через 1 секунду
    print("Выполнено с задержкой")
}

Поговорим про инструменты GCD

Grand Central Dispatch (GCD) предлагает несколько инструментов для управления многопоточной работой.

1. Очереди (Dispatch Queues). Это основные инструменты для выполнения кода асинхронно или в разных потоках. Очереди выполняют задачи в FIFO-порядке (первый пришел — первый обслужен). Они могут быть серийными (выполняют одну задачу за другой) или параллельными (выполняют несколько задач одновременно).

// Создание и использование фоновой очереди с последующим возвратом на основной поток:
let backgroundQueue = DispatchQueue.global(qos: .background)

backgroundQueue.async {
    // выполняется в фоновом потоке
    print("This is run on the background queue")

    DispatchQueue.main.async {
        // выполняется на основном потоке
        print("This is run on the main queue")
    }
}

2. Группы (Dispatch Groups). Позволяют сгруппировать задачи и уведомить вас, когда все задачи в группе завершены. Это особенно полезно, когда вам нужно выполнить набор параллельных операций и дождаться их завершения.

//Выполнение нескольких асинхронных задач и ожидание их завершения:
let group = DispatchGroup()

group.enter()
someAsyncFunction1() {
    group.leave()
}

group.enter()
someAsyncFunction2() {
    group.leave()
}

group.notify(queue: .main) {
    print("All async tasks have completed")
}

3. Семафоры (Dispatch Semaphores). Семафоры предоставляют механизм синхронизации для регулирования доступа к общим ресурсам.

// Использование семафора для ограничения одновременного доступа к общему ресурсу:

let semaphore = DispatchSemaphore(value: 1)  // начальное значение 1
var data = SharedResource()

DispatchQueue.global().async {
    semaphore.wait()  // уменьшает значение семафора
    // критическая секция
    data.modify()
    semaphore.signal()  // увеличивает значение семафора
}

4. Исключительный доступ (Dispatch Barriers). Барьеры используются для создания точек синхронизации внутри параллельных очередей. Барьерная задача будет ждать, пока все задачи, добавленные в очередь до барьера, будут выполнены, после чего она сама начнет выполняться. Все задачи, добавленные после барьера, будут ждать, пока барьерная задача завершится.

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

swift
Copy code
let queue = DispatchQueue(label: "com.example.myqueue", attributes: .concurrent)
var data = SharedResource()

queue.async {
    data.read()
}

queue.async(flags: .barrier) {
    data.write()
}

queue.async {
    data.read()
}

5. Источники событий (Dispatch Sources). Это механизмы для обработки системных уведомлений и событий на низком уровне.

Dispatch Sources в GCD — это мощный механизм для мониторинга различных системных событий. Примером такого события может быть изменение содержимого файла, входящий сетевой пакет, сигнал Unix и т.д.

Пример использования Dispatch Sources для мониторинга изменения файла:

// Путь к файлу, который вы хотите наблюдать
let path = "/path/to/file"

// Получение дескриптора файла
let fileDescriptor = open(path, O_EVTONLY)

if fileDescriptor != -1 {
    // Создание Dispatch Source
    let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: DispatchQueue.global())

    // Установка обработчика события
    source.setEventHandler {
        // Код, который будет выполнен, когда файл изменится
        print("\(path) has changed.")
    }

    // Установка обработчика отмены
    source.setCancelHandler {
        close(fileDescriptor)
    }

    // Запуск Dispatch Source
    source.resume()
} else {
    print("Unable to open path: \(path)")
}

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

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

6. Очереди таймеров (Dispatch Timers). Это механизмы для создания таймеров, которые выполняют код в определенное время или в заданные интервалы времени.

// Пример таймера, который выполняет блок кода каждые две секунды:

// Создание Dispatch Timer
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())

// Настройка таймера на повтор каждые две секунды
timer.schedule(deadline: .now(), repeating: .seconds(2))

// Установка обработчика события
timer.setEventHandler {
    print("Timer fired at \(Date())")
}

// Запуск таймера
timer.resume()

// Если вы хотите остановить таймер, вы можете сделать это, вызвав метод cancel():

timer.cancel()

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

Grand Central Dispatch — мощный и гибкий инструмент для управления многопоточностью в Swift. С его помощью вы сможете улучшить производительность и отзывчивость вашего приложения.

Проблемы, встречающиеся в GCD

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

Deadlock (взаимная блокировка)

Deadlock возникает, когда два или более потока навсегда блокируют друг друга, ожидая ресурсов, которые удерживают друг друга. Например, если вы вызываете sync на текущей очереди, это приведет к deadlock.

DispatchQueue.main.sync {
    // Этот блок никогда не выполнится, так как вызов `sync`
    // будет ждать, пока блок не завершится, а блок будет ждать, 
    // пока `sync` не вернет управление.
}

Race conditions (условия гонки)

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

var counter = 0
let queue = DispatchQueue(label: "com.example.myqueue", attributes: .concurrent)

for _ in 0..<1000 {
    queue.async {
        counter += 1
    }
}

print(counter) // Это может быть любое число, не обязательно 1000

Priority inversion (обратное изменение приоритета)

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

let highPriority = DispatchQueue.global(qos: .userInitiated)
let lowPriority = DispatchQueue.global(qos: .utility)

let semaphore = DispatchSemaphore(value: 1)

lowPriority.async {
    semaphore.wait()
    Thread.sleep(forTimeInterval: 2) // Эмуляция затратной операции
    semaphore.signal()
}

highPriority.async {
    semaphore.wait()
    // Этот блок должен быть выполнен быстро, 
    // но он должен ждать, пока низкоприоритетная задача не освободит семафор
    semaphore.signal()
}

Resource starvation (истощение ресурсов)

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

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

OperationQueue в Swift: погружение в многопоточность

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

Введение в OperationQueue

OperationQueue — это высокоуровневый API для управления очередями работ в Swift и Objective-C, построенный на основе Grand Central Dispatch (GCD). OperationQueue предлагает большую гибкость по сравнению с GCD за счет возможности отмены операций, установки зависимостей между операциями и управления очередью операций.

// Создаем новую операцию
let operation = BlockOperation {
    print("Hello from operation!")
}

// Создаем новую очередь операций
let queue = OperationQueue()

// Добавляем операцию в очередь
queue.addOperation(operation)

Зависимости операций

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

let firstOperation = BlockOperation {
    print("First operation")
}

let secondOperation = BlockOperation {
    print("Second operation")
}

secondOperation.addDependency(firstOperation)

let queue = OperationQueue()
queue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)

Отмена операций

Еще одно преимущество использования OperationQueue — это возможность отмены операций. Вы можете отменить отдельные операции или все операции в очереди.

let operation = BlockOperation {
    print("Operation started")
    Thread.sleep(forTimeInterval: 2)
    print("Operation finished")
}

let queue = OperationQueue()
queue.addOperation(operation)

// Отменить операцию
operation.cancel()

Ограничение параллелизма

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

let queue = OperationQueue()

// Ограничиваем максимальное количество операций, выполняемых одновременно, до 2
queue.maxConcurrentOperationCount = 2

Встречающиеся проблемы при использовании OperationQueue:

  1. Отмена операций. Хотя OperationQueue предоставляет простой способ отмены операций, важно помнить, что операция не прекращается немедленно, когда вызывается метод cancel(). Вместо этого статус isCancelled устанавливается в true, и ваш код должен регулярно проверять этот статус и корректно реагировать на его изменение.

  2. Зависимости операций. Управление зависимостями между операциями может привести к сложным сценариям, особенно если создается циклическая зависимость (операция A зависит от операции B, а операция B зависит от операции A), что приводит к deadlock.

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

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

NSLock в Swift: управление доступом к общим ресурсам

Многопоточное программирование может стать сложным, особенно когда нужно синхронизировать доступ к общим ресурсам. В Swift и Objective-C для этого предусмотрен инструмент NSLock, который представляет собой примитив синхронизации, обеспечивающий взаимоисключающий доступ к общим ресурсам.

Как работает NSLock

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

let lock = NSLock()
var data = [String]()

DispatchQueue.global().async {
    lock.lock()
    data.append("Write data")
    lock.unlock()
}

DispatchQueue.global().async {
    lock.lock()
    print(data)
    lock.unlock()
}

Когда использовать NSLock

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

Проблемы, возникающие при использовании NSLock

1. Deadlock. Если поток пытается закрыть замок, который он уже закрыл, произойдет deadlock. Вот пример, демонстрирующий это:

let lock = NSLock()

DispatchQueue.global().async {
    lock.lock()
    print("First lock acquired")
    
    lock.lock() // Deadlock
    print("Second lock acquired")
}

В этом примере второй вызов lock() приведет к deadlock, поскольку замок уже закрыт этим потоком.

2. Отсутствие исключений. В отличие от некоторых других примитивов синхронизации, таких как NSRecursiveLock, NSLock не генерирует исключения, если произошла ошибка, такая как попытка повторного захвата замка, который уже закрыт. Это может усложнить отладку проблем.

3. Утечки ресурсов. Если поток, закрывший замок, по какой-то причине не может его открыть (например из-за ошибки или исключения), то замок останется закрытым навсегда, и другие потоки никогда не смогут его закрыть. Это приведет к утечке ресурсов.

4. Отсутствие приоритета. NSLock не обеспечивает приоритеты для потоков. Это означает, что все потоки, ожидающие закрытия замка, получают доступ к нему в порядке прихода, независимо от их приоритета.

Вывод: NSLock — это полезный инструмент для синхронизации доступа к общим ресурсам в Swift и Objective-C. Однако необходимо помнить о возможных проблемах и использовать этот инструмент с осторожностью.

P.S. Await и async в Swift: новые горизонты многопоточности

С появлением Swift 5.5 Apple представила новые ключевые слова — async и await, которые значительно упрощают работу с асинхронными операциями и делают код более читаемым.

Понимание async/await

Async и await — это ключевые слова, добавленные в Swift 5.5 для упрощения написания асинхронного кода. Async указывает, что функция, метод или замыкание выполняются асинхронно, то есть могут выполняться в фоновом режиме и завершаться в любое время. Await используется для обозначения точек в коде, где вы ожидаете завершения асинхронной операции.

func fetchData() async -> Data {
    // Fetch data from a server or perform some long-running computation
    // ...
}

func process() async {
    let data = await fetchData()
    // Process the data
    // ...
}

Async/await и многопоточность

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

В Swift существуют другие инструменты, такие как Grand Central Dispatch (GCD) и OperationQueues, которые предоставляют возможность контроля над многопоточностью. Они позволяют задать очереди выполнения для ваших асинхронных задач, что помогает более точно управлять тем, где и когда выполняются эти задачи.

Примеры использования async/await

Рассмотрим пример асинхронной загрузки данных из сети.

func fetchImage() async throws -> UIImage {
    let url = URL(string: "https://example.com/image.jpg")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return UIImage(data: data)!
}

async {
    do {
        let image = try await fetchImage()
        // Update the UI on the main thread
        DispatchQueue.main.async {
            imageView.image = image
        }
    } catch {
        // Handle error
    }
}

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

Итак, async и await представляют собой сильные инструменты для написания чистого, понятного асинхронного кода в Swift. Они не заменяют существующие механизмы многопоточности, но делают работу с асинхронным кодом более простой и интуитивно понятной.

Заключение

В заключение хочу сказать, что многопоточность — это ключевой элемент разработки программного обеспечения, который позволяет создавать более эффективные и быстрые приложения. В Swift есть множество инструментов, которые помогают нам в этом, начиная от основных механизмов управления потоками, таких как GCD и OperationQueues, заканчивая новейшими добавлениями, такими как async/await.

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

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

Благодарю за внимание и желаю успехов в ваших разработках на Swift!

Комментарии (3)


  1. nvolnikov
    21.06.2023 18:15
    +1

    Интересная статья, спасибо


  1. debug45
    21.06.2023 18:15
    +1

    Под заголовком о Race Condition описан не Race Condition, а Data Race


    1. Chulakov_Dev Автор
      21.06.2023 18:15

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

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