Здравствуйте, уважаемые читатели Хабра! Меня зовут Кирилл, я iOS-разработчик приложений Сбера в Студии Олега Чулакова. Когда я не занят написанием кода, мне нравится изучать новые технологии и помогать другим разработчикам улучшать их навыки через менторство. Это всегда интересный и увлекательный процесс, который помогает мне самому оставаться в курсе последних тенденций в профессии. Сегодня я хочу поговорить о важной теме, которая стоит в центре многих дискуссий в сообществе разработчиков, — многопоточности в Swift. Эта тема может казаться сложной и запутанной, но я уверен, что с правильным подходом и набором инструментов она станет более доступной. В этой статье мы разберемся с основами многопоточности и познакомимся с такими инструментами Swift, как Grand Central Dispatch (GCD), OperationQueues, NSLock. А также существующими низкоуровневыми технологиями, такими как pthread и NSThread. Бонусом я расскажу про async/await. Мы обсудим, как эти инструменты помогают управлять асинхронными и параллельными операциями, и посмотрим на некоторые примеры их использования. Приятного чтения, и давайте начнем наше путешествие в мир многопоточности Swift! Многопоточность — это аспект конкурентного программирования, позволяющий программе выполняться в нескольких потоках одновременно. Это улучшает производительность, поскольку позволяет более эффективно использовать процессорное время и проводить операции в фоновом режиме. Суть многопоточности — разделение работы на меньшие задачи, которые можно выполнять параллельно. Однако важно помнить, что многопоточность не всегда является панацеей. Иногда она приводит к сложностям в управлении состоянием и синхронизации. Swift предлагает различные методы реализации многопоточности, такие как Grand Central Dispatch (GCD), OperationQueue, NSLock. И низкоуровневые технологии, такие как pthread и NSThread. POSIX Threads (pthread) — это стандартный интерфейс для работы с потоками в операционных системах UNIX. pthread предоставляет набор функций и типов данных, которые позволяют создавать и управлять потоками выполнения. Хотя pthread — это низкоуровневый интерфейс, он предлагает гибкость и полный контроль над созданием и управлением потоками. Для создания нового потока с использованием pthread в Swift, вам необходимо выполнить следующие шаги: Данный код демонстрирует создание нового потока с помощью pthread_create(). Вы также можете определить свою функцию потока, которая будет выполняться в новом потоке. После создания потока вы можете присоединиться к нему с помощью pthread_join(), чтобы дождаться его завершения. NSThread — это класс Objective-C, который предоставляет более высокоуровневый интерфейс для работы с потоками в iOS и macOS. NSThread основан на pthread и предоставляет удобные методы для создания, управления и синхронизации потоков. Пример использования NSThread: В этом примере мы используем класс Thread для создания нового потока и указываем целевой объект (self) и селектор функции потока (#selector(threadFunction)). Затем мы запускаем поток с помощью метода start(). Для остановки потока мы вызываем метод cancel() и устанавливаем ссылку на поток в nil. При выборе между pthread и NSThread важно учитывать контекст вашего проекта и требования к многопоточности. pthread обеспечивает более низкоуровневый доступ и большую гибкость, но требует большего количества кода для создания и управления потоками. NSThread, с другой стороны, предлагает более высокоуровневый интерфейс и интеграцию с Objective-C, что делает его более удобным для использования. Однако стоит отметить, что начиная с iOS 10 и macOS 10.12, Apple рекомендует использовать более современные и высокоуровневые подходы к многопоточности, такие как Grand Central Dispatch (GCD) и Operation Queues. Эти подходы предоставляют более удобный и безопасный способ работы с многопоточностью, особенно в контексте асинхронных операций. В итоге выбор между pthread и NSThread зависит от ваших потребностей и предпочтений. Оба инструмента имеют свои преимущества и недостатки, и правильный выбор зависит от специфики вашего проекта. Grand Central Dispatch (GCD) — это низкоуровневая библиотека, разработанная Apple, позволяющая управлять параллельными задачами в многопоточном окружении. В центре концепции GCD лежат очереди задач и их выполнение в различных потоках. GCD использует два типа очередей: серийные и параллельные. Серийные (Serial) очереди выполняют одну задачу за другой. Когда одна задача завершается, начинается следующая. Это очень полезно, когда вы хотите, чтобы задачи выполнялись строго в определенном порядке. Это также помогает избежать проблем с состоянием гонки, поскольку в любой момент времени в серийной очереди выполняется только одна задача. Параллельные (Concurrent) очереди могут выполнять несколько задач одновременно. Задачи начинают выполняться в том порядке, в котором они были добавлены в очередь, но они могут закончиться в любом порядке. Таким образом, следующая задача может начаться даже до того, как предыдущая завершилась. Давайте разберемся с основами и рассмотрим несколько примеров. Очереди выполнения в GCD — это места, куда вы помещаете задачи для выполнения. В Swift существуют три типа очередей. 1. Основная очередь (Main Queue). Выполняется в основном потоке и используется для обновления пользовательского интерфейса. Это последовательная очередь. 2. Глобальная очередь (Global Queue). Это системная предоставленная очередь, которая выполняется в фоновом режиме. Это параллельная очередь, и она бывает разных уровней приоритета. 3. Пользовательская очередь (Custom Queue). Программист может создавать собственные очереди. Эти очереди могут быть последовательными или параллельными. GCD выполняет задачи асинхронно или синхронно. 1. Асинхронные задачи возвращают управление на текущую очередь немедленно, не дожидаясь их завершения. 2. Синхронные задачи не возвращают управление, пока задача не завершена.  Важно помнить, что обновление пользовательского интерфейса должно всегда выполняться в основном потоке. GCD позволяет запланировать выполнение задачи с задержкой. Grand Central Dispatch (GCD) предлагает несколько инструментов для управления многопоточной работой. 1. Очереди (Dispatch Queues). Это основные инструменты для выполнения кода асинхронно или в разных потоках. Очереди выполняют задачи в FIFO-порядке (первый пришел — первый обслужен). Они могут быть серийными (выполняют одну задачу за другой) или параллельными (выполняют несколько задач одновременно). 2. Группы (Dispatch Groups). Позволяют сгруппировать задачи и уведомить вас, когда все задачи в группе завершены. Это особенно полезно, когда вам нужно выполнить набор параллельных операций и дождаться их завершения. 3. Семафоры (Dispatch Semaphores). Семафоры предоставляют механизм синхронизации для регулирования доступа к общим ресурсам. 4. Исключительный доступ (Dispatch Barriers). Барьеры используются для создания точек синхронизации внутри параллельных очередей. Барьерная задача будет ждать, пока все задачи, добавленные в очередь до барьера, будут выполнены, после чего она сама начнет выполняться. Все задачи, добавленные после барьера, будут ждать, пока барьерная задача завершится. 5. Источники событий (Dispatch Sources). Это механизмы для обработки системных уведомлений и событий на низком уровне. Dispatch Sources в GCD — это мощный механизм для мониторинга различных системных событий. Примером такого события может быть изменение содержимого файла, входящий сетевой пакет, сигнал Unix и т.д. Пример использования Dispatch Sources для мониторинга изменения файла: В этом примере создается Dispatch Source, который наблюдает за записью в файл. Когда файл изменяется, выполняется обработчик события. Когда Dispatch Source отменяется, дескриптор файла закрывается. Пожалуйста, обратите внимание, что этот пример представляет собой простую демонстрацию возможностей Dispatch Sources. В реальном приложении вам следует обработать ошибки более тщательно и убедиться, что вы правильно управляете ресурсами системы. 6. Очереди таймеров (Dispatch Timers). Это механизмы для создания таймеров, которые выполняют код в определенное время или в заданные интервалы времени. Важно отметить, что, несмотря на мощь и гибкость этих инструментов, многопоточное программирование всегда требует внимательного управления состояниями и данными, чтобы избежать проблем с синхронизацией и состоянием гонки. Grand Central Dispatch — мощный и гибкий инструмент для управления многопоточностью в Swift. С его помощью вы сможете улучшить производительность и отзывчивость вашего приложения. Многопоточное программирование всегда связано с определенными проблемами и сложностями, и Grand Central Dispatch (GCD) не является исключением. Давайте рассмотрим несколько проблем, которые могут возникнуть при использовании GCD. Deadlock возникает, когда два или более потока навсегда блокируют друг друга, ожидая ресурсов, которые удерживают друг друга. Например, если вы вызываете sync на текущей очереди, это приведет к deadlock. Условия гонки могут возникнуть, когда два или более потока имеют доступ к общему ресурсу и хотя бы один из них выполняет запись. Если порядок записи важен, это может привести к непредсказуемым результатам. Priority inversion происходит, когда поток с более низким приоритетом удерживает ресурс, который требуется потоку с более высоким приоритетом. Это может привести к тому, что потоки с высоким приоритетом будут ожидать, пока потоки с низким приоритетом освободят ресурсы, что противоречит ожидаемому поведению. Resource starvation происходит, когда некоторые потоки не получают доступ к ресурсам, потому что другие потоки постоянно их занимают. Это может произойти, например, если вы создаете большое количество задач с высоким приоритетом, которые занимают все потоки в пуле потоков, что приводит к тому, что задачи с низким приоритетом не получают шанса запуститься. Эти проблемы являются общими для многопоточного программирования, и важно понимать их и уметь с ними работать при использовании GCD. Хорошо спроектированная многопоточная система учитывает эти проблемы и применяет соответствующие стратегии для их минимизации. Одной из основных задач при разработке программного обеспечения является эффективное использование многоядерных процессоров. Swift предоставляет несколько способов для выполнения задач в параллельных потоках, одним из которых является OperationQueue. OperationQueue — это высокоуровневый API для управления очередями работ в Swift и Objective-C, построенный на основе Grand Central Dispatch (GCD). OperationQueue предлагает большую гибкость по сравнению с GCD за счет возможности отмены операций, установки зависимостей между операциями и управления очередью операций. Одним из ключевых преимуществ использования OperationQueue является возможность установки зависимостей. Вы можете управлять порядком выполнения операций, устанавливая одну операцию зависимой от другой. Еще одно преимущество использования OperationQueue — это возможность отмены операций. Вы можете отменить отдельные операции или все операции в очереди. Еще одной особенностью OperationQueue является возможность управления степенью параллелизма операций, то есть максимальным количеством операций, выполняемых одновременно. Встречающиеся проблемы при использовании OperationQueue: Отмена операций. Хотя OperationQueue предоставляет простой способ отмены операций, важно помнить, что операция не прекращается немедленно, когда вызывается метод cancel(). Вместо этого статус isCancelled устанавливается в true, и ваш код должен регулярно проверять этот статус и корректно реагировать на его изменение. Зависимости операций. Управление зависимостями между операциями может привести к сложным сценариям, особенно если создается циклическая зависимость (операция A зависит от операции B, а операция B зависит от операции A), что приводит к deadlock. Отслеживание выполнения операций. По умолчанию Operation не предоставляет простого способа отслеживания прогресса операции или получения уведомления при ее завершении. Это можно решить путем подклассификации Operation и добавления таких функций самостоятельно, но это требует дополнительной работы. Вывод: OperationQueue является мощным и гибким инструментом для управления многопоточными операциями в Swift. Его возможности делают его особенно полезным для сложных сценариев, где нужно контролировать зависимости, отмену или степень параллелизма операций. Многопоточное программирование может стать сложным, особенно когда нужно синхронизировать доступ к общим ресурсам. В Swift и Objective-C для этого предусмотрен инструмент NSLock, который представляет собой примитив синхронизации, обеспечивающий взаимоисключающий доступ к общим ресурсам. NSLock работает как дверной замок: только один поток может «заключить» его в любой момент времени. Пока замок закрыт, все остальные потоки, которые пытаются его закрыть, будут заблокированы до тех пор, пока замок не будет открыт. NSLock полезен в ситуациях, когда вам нужно защитить критическую секцию кода, которая обновляет общий ресурс, такой как общий объект или структура данных. Однако NSLock следует использовать осторожно, так как неправильное применение может привести к таким проблемам, как взаимные блокировки или утечки памяти. 1. Deadlock. Если поток пытается закрыть замок, который он уже закрыл, произойдет deadlock. Вот пример, демонстрирующий это: В этом примере второй вызов lock() приведет к deadlock, поскольку замок уже закрыт этим потоком.  2. Отсутствие исключений. В отличие от некоторых других примитивов синхронизации, таких как NSRecursiveLock, NSLock не генерирует исключения, если произошла ошибка, такая как попытка повторного захвата замка, который уже закрыт. Это может усложнить отладку проблем. 3. Утечки ресурсов. Если поток, закрывший замок, по какой-то причине не может его открыть (например из-за ошибки или исключения), то замок останется закрытым навсегда, и другие потоки никогда не смогут его закрыть. Это приведет к утечке ресурсов. 4. Отсутствие приоритета. NSLock не обеспечивает приоритеты для потоков. Это означает, что все потоки, ожидающие закрытия замка, получают доступ к нему в порядке прихода, независимо от их приоритета. Вывод: NSLock — это полезный инструмент для синхронизации доступа к общим ресурсам в Swift и Objective-C. Однако необходимо помнить о возможных проблемах и использовать этот инструмент с осторожностью. С появлением Swift 5.5 Apple представила новые ключевые слова — async и await, которые значительно упрощают работу с асинхронными операциями и делают код более читаемым. Async и await — это ключевые слова, добавленные в Swift 5.5 для упрощения написания асинхронного кода. Async указывает, что функция, метод или замыкание выполняются асинхронно, то есть могут выполняться в фоновом режиме и завершаться в любое время. Await используется для обозначения точек в коде, где вы ожидаете завершения асинхронной операции. Ключевые слова async и await предназначены для упрощения асинхронного программирования, но они не являются самим механизмом многопоточности. Они позволяют вам написать код, который может выполняться асинхронно, но они не контролируют, в каком потоке этот код будет выполняться. В Swift существуют другие инструменты, такие как Grand Central Dispatch (GCD) и OperationQueues, которые предоставляют возможность контроля над многопоточностью. Они позволяют задать очереди выполнения для ваших асинхронных задач, что помогает более точно управлять тем, где и когда выполняются эти задачи. Рассмотрим пример асинхронной загрузки данных из сети. В этом примере функция fetchImage() объявлена как async, что означает, что она может выполняться асинхронно. Мы затем вызываем эту функцию с использованием await внутри асинхронного замыкания, что означает, что мы ожидаем завершения этой функции, прежде чем продолжим выполнять следующий код. Итак, async и await представляют собой сильные инструменты для написания чистого, понятного асинхронного кода в Swift. Они не заменяют существующие механизмы многопоточности, но делают работу с асинхронным кодом более простой и интуитивно понятной. В заключение хочу сказать, что многопоточность — это ключевой элемент разработки программного обеспечения, который позволяет создавать более эффективные и быстрые приложения. В Swift есть множество инструментов, которые помогают нам в этом, начиная от основных механизмов управления потоками, таких как GCD и OperationQueues, заканчивая новейшими добавлениями, такими как async/await. Освоение этих инструментов требует времени и практики, но оно стоит усилий. Возможность эффективно использовать многопоточность открывает новые горизонты для разработки, позволяя создавать приложения, которые могут более эффективно применять ресурсы устройства и предоставлять лучший пользовательский опыт. Надеюсь, статья помогла вам лучше понять, как работает многопоточность в Swift, и вдохновила на дальнейшее изучение этой захватывающей темы. Не бойтесь экспериментировать и пробовать новые подходы: это один из лучших способов профессионально развиваться. Благодарю за внимание и желаю успехов в ваших разработках на Swift!  Разработчик № 1 для банков