ios

Материалы по платформе iOS.

View on GitHub

Back

Multithreading. Part 1 Overview, GCD

Basic concepts

Process (процесс) - простыми словами это наше запущенное приложение. Под процесс выделяется какой-то блок памяти, адресное пространство которого может быть использовано только внутри этого процесса. Процесс имеет потоки. Thread (поток) — это последовательность инструкций, которые могут быть выполнены во время выполнения приложения.

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

Можно ли реализовать многопоточность на одноядерном процессоре? Да, можно. Однако это будет многопоточность основанная на смене контекста, это значит, что одновременно смогут работать только то количество потоков, которое соответствует количеству ядер и периодически система будет приостанавливать эти потоки и запускать в работу другие и передавая им процессорное время. Т.е. потоки работают не на 100%, а с некоторыми прерываниями и ожиданиями для возвращения в работу.

При старте iOS приложение имеет только один поток — это главный поток(main). На главном потоке обновляется пользовательский интерфейс и выполняются все операции, непосредственно связанные с пользовательским интерфейсом. Соответственно не нужно выполнять все задачи на главном потоке и “тормозить” интерфейс. Задачи, которые требуют некоторого времени, целесообразно выносить в отдельный поток.

Multiple threads advantages

Преимущества многопоточности

Thread types in iOS

Типы потоков:

Звучит классно, да? Больше ядер, больше потоков, осталось только распаралелить наше приложение как можно сильнее. Но не так быстро. Реальность такова что существует очень маленький шанс что нам придется манипулировать потоками напрямую. Apple предоставляет API для манипулирования потоками без вашего участия.

Grand Central Dispatch (GCD)

Цель GCD - поставить в очередь задачи, метод, либо блок - которые могут выполняться параллельно, в зависимости от доступности ресурсов. GCD управляет пулом потоков под капотом. GCD решает сам, в каком конкретном потоке будут выполняться ваши блоки кода.

Queue creation

dispatch_queue_t queue = dispatch_queue_create("com.rsschool.concurrency.demo", 0);

Когда вы создаете очередь, операционная система создает, или назначает для этой очереди один или несколько потоков. Если существующие потоки доступны, они могут быть переиспользованы. Функция создания очереди dispatch_queue_create принимает уникальный идентификатор и тип очереди. Типов очереди бывает два.

Queue types: Оба типа реализуют принцип FIFO.

dispatch_queue_t queue = dispatch_queue_create("com.rsschool.concurrency.demo", 0);
// or
dispatch_queue_t queue = dispatch_queue_create("com.rsschool.concurrency.demo",DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue = dispatch_queue_create("com.rsschool.concurrency.demo", DISPATCH_QUEUE_CONCURRENT);

Concurrent queues priorities

В системе есть четыре глобальные очереди с разными приоритетами.

Все глобальные очереди являются конкурентными.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

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

Quality of service (QoS)

Начиная с iOS 8 на замену обычным приоритетам, Apple предоставила другой способ приоритезации очередей. По большому счету как таковых отличий нет, просто абстрактные приоритеты приобрели более осмысленные названия.

Получить глобальную очередь используя QoS можно точно так-же как и с обычным приоритетом, только вместо приоритета в качестве первого параметра передаем тип QoS:

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);

Adding tasks to queues

Задачи в очередь можно добавить двумя способами:

!!!Важно!!! Асинхронно не значит конкуренто!!!

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

Sync adding tasks to queues

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

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_sync(queue, ^{
	// Some task
});

Async adding tasks to queues

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

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
   dispatch_async(queue, ^{
     	// Some task
   });

Dispatch group

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

Dispatch group creation

Работа с группой начинается с ее создания:

dispatch_group_t group = dispatch_group_create();

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

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_async(group, queue, ^{ 
    // Some task
});

То, что мы передаем очередь в качестве параметра, говорит о том, что группа не привязана к какой-то конкретной очереди. Задачи в группе могут выполняться на разных очередях. Dispatch group предоставляет функцию dispatch_group_notify, которую можно использовать чтобы узнать, что все задачи в группе были выполнены. В данную функцию в качестве первого параметра передается отслеживаемая группа, вторым параметром передается очередь, на которой должен быть выполнен блок, передаваемый третьим параметром.

dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 
    // Some completion task
});

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

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

Функция блокирует текущую очередь на время передаваемое в качестве второго параметра.

Wrapping async methods

DispatchQueue знает, как работает DispatchGroup и знает, как отслеживать завершение задач. Однако это касается синхронных блоков кода. Асинхронные вызовы не отслеживаются автоматически. Для того чтобы сообщить группе что асинхронный вызов начался и закончился, для этого можно использовать функции dispatch_group_enter и dispatch_group_leave. Вызов dispatch_group_enter увеличивает счетчик выполняемых операций, а dispatch_group_leave уменьшает этот счетчик.

dispatch_group_async(group, queue, ^{ 
    // Some task
    dispatch_group_enter(group); 
    dispatch_async(queue, ^{
        // Perform some work
        dispatch_group_leave(group);
    });
});

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

Multithreading pitfalls

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

Race condition

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

Например, у нас есть значение, также имеются два потока. Каждый поток в единицу времени может выполнять одну операцию. В единицу времени поток А читает значение, на второй единице времени поток А делает сложение. В это время поток В считывает значение переменной, так как поток А сделал сложение, но еще не записал значение, поток 2 считал значение 10. На третьем промежутке времени поток А записывает результат сложения в переменную, а поток В одновременно делает операцию сложения значению которое прочитал ранее. На четвертом промежутке времени поток В записывает получившееся значение. В результате мы получаем неверное значение. Работа потоков зависит от множества факторов, поэтому каждый запуск приложения может давать разные результаты.

Deadlock

Взаимная блокировка потоков.

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

Priority inversion

Скорость работы высоко-приоритетного потока равна или ниже скорости работы потока с низким приоритетом

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

Synchronization tools

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

Barriers

Например, у нас есть поток задач, например задача чтения и задача записи. Эти задачи вы асинхронно добавляете на конкурентную очередь, т.е. все задачи могут выполняться параллельно. Если для задач чтения это нормально, то для задач записи совместно с задачи чтения нет. Т.е типичная проблема Race Condition. Нам нужно сделать так, чтобы при выполнении задачи записи, никакие другие задачи не выполнялись.

Мы можем сделать задачу записи т.н. барьером. Когда выполняется барьер, никакие другие задачи выполняются не могут. Если при добавлении барьера какие-то задачи были в процессе выполнения, система завершит эти задачи, и только после этого стартует барьер. Никакие новые задачи не начнут свою работу пока не завершится барьер. Т.е. можно сказать, что наша конкурентная очередь на время выполнения стала серийной. Для добавления барьера используем функцию dispatch_barrier_async.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_barrier_async(queue, ^{ // Some barrier task
});

Semaphore

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

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

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
   dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
   // Some task with shared resource 
   dispatch_semaphore_signal(semaphore);
});

Mutex

Mutex — это частный случай семафора. Это семафор, при котором только один поток может занимать ресурс в единицу времени.

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

NSLock *theLock = [NSLock new];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
    if ([theLock tryLock]) {
        // Some task
        [theLock unlock];
    }
});

Создаем объект NSLock. Для блокировки ресурса в начале задачи вызывает tryLock, для разблокировки ресурса в конце задачи вызываем unlock.

Recursive mutex

Давайте рассмотрим пример с обычным мьютексом. Поток 1 пытается обратится к ресурсу, который он уже занял, мьютекс разрешает обращение только один раз. Например, у нас есть рекурсивный вызов какого-то метода, внутри которого мы обращаемся к одному и тому же ресурсу, эта попытка приведет к deadlock. Чтобы разрешить эту проблему можно использовать recursive mutex. Recursive mutex это тот же мьютекс, который разрешает одному и тому же потоку обращатся к ресурсу несколько раз.

Для использования рекурсивного мьютекса используем класс NSRecursiveLock, подход по его использованию точно такой же, как и при использовании NSLock.

NSRecursiveLock *theRecursiveLock = [NSRecursiveLock new];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
    if ([theRecursiveLock tryLock]) {
        // Some task
        [theRecursiveLock unlock];
    }
});

Useful materials 🤓

Apple. Threading Programming Guide

Apple. Dispatch