- Многопоточность (concurrency) в Swift 3. GCD и Dispatch Queues
- Dispatch Queues
- Многопоточность: Runloop, Многопоточность в iOS и macOS, Deadlock, Livelock, DispatchGroup, Синхронные и асинхронные задачи, @synchronized, Мьютекс, Семафор
- Problem Of Concurrency
- Race Conditions and Critical Sections
Как только мы позволяем задачам (tasks) работать параллельно, появляются проблемы, связанные с тем, что разные задачи захотят получить доступ к одним и тем же ресурсам. Основных проблемы три:
О шибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Когда несколько потоков обращаются к одному и тому же куску кода в памяти (сritical section), и результат может различаться в зависимости от последовательности, в которой выполняются потоки, говорят, что критическая секция содержит состояние гонки.
Critical section - это секция кода, которыя выполнятся несколькими потоками.
Пример №1
Пример взят отсюда.
// 1
var value: Int = 0
let serialQueue = DispatchQueue(label: "ru.popov.serial-queue")
// 2
func increment() { value += 1 }
// 3
serialQueue.async {
// 4
sleep(5)
increment()
}
// 5
print(value)
// 6
value = 10
// 7
serialQueue.sync {
increment()
}
// 8
print(value) // 12
-
Создаем свойство value и последовательную очередь serialQueue
-
Описываем функцию инкрементирования value
-
Планируем задачу и сразу же возвращаем управление вызывающей очереди
-
Имитируем продолжительную работу усыпляя поток и тут же вызываем функцию increment
-
Выводим в консоль значение переменной value, получаем 0 и вот тут начинается самое интересное. Для полноты картины представьте, что начиная с этого пункта и до конца сниппета, код находится в другой части приложения, а зависимости (value, serialQueue) переданы через DI. То есть вы и понятия не имеете, что через 5 секунд value будет инкрементирован. Мы получаем в консоли значение 0 и для нас это своего рода source of truth.
-
Передаем в переменную value новое значение
-
На этот раз инкрементируем синхронно
-
Снова выводим значение value в консоль. Ожидаем получить 11, но получаем 12.
Попробуем визуализировать пример:
Чтобы решить нашу, достаточно синхронизировать вызывающую очередь и serialQueue, тогда мы сможем гарантировать работу с актуальным значением value:
var value: Int = 0
let serialQueue = DispatchQueue(label: "ru.popov.serial-queue")
func increment() { value += 1 }
serialQueue.sync {
sleep(5)
increment()
}
print(value)
value = 10
serialQueue.sync {
increment()
}
print(value) // 11
И снова визуализируем:
Race condition является одной из самых сложно отлавливаемых (но не самых страшных) проблем. Проще избежать, чем исправлять, поэтому к проектированию многопоточного кода нужно подходить ответственно и с умом.
Решение проблемы:
- Actor;
- примитивы мьютекса
Логическое несоответствие с правилами планирования — задача с более высоким приоритетом находится в ожидании в то время как низкоприоритетная задача выполняется;
Низкоприоритетная захватывает ресурс и не отдает его более важной по приоритету задаче и высокоприоритетная ждет выполнения низкоприоритетной задачи.
Ситуация в многопоточной системе, при которой несколько потоков находятся в состоянии бесконечного ожидания ресурсов, занятых самими этими потоками.
В swift возникает, когда очередь вызывает sync внутри самой себя
Пример: Представьте, что у нас есть два человека, Джон и Майкл, которые одновременно пытаются пройти через узкий дверной проход из противоположных сторон. Они оба встречаются в центре прохода и ни один из них не может двигаться вперед, пока другой не отойдет назад. Однако, оба отказываются отступить, и таким образом они оба остаются заблокированными в проходе.
Пример
Первое закрытие не может быть завершено до тех пор, пока не будет завершено второе закрытие:
let serialQueue = DispatchQueue(label: "com.popov.app.exampleQueue")
serialQueue.sync {
// ...
serialQueue.sync { // deadlock
// ...
}
}
❗ НИКОГДА НЕ вызывайте метод sync на main queue, потому что это приведет к взаимной блокировке (deadlock) вашего приложения!
Замкнутая ситуация: основной поток ждет завершения блока, который не может быть выполнен, потому что основной поток занят ожиданием;
// на главной очереди
DispatchQueue.main.sync { // deadlock
}
Livelock возникает, когда два или более процессов или потоков блокируются и неоднократно пытаются получить ресурс или выполнить операцию, но ни один из них не может добиться прогресса.
Пример: Два человека звонят друг другу по телефону и оба обнаруживают, что линия занята. Оба джентльмена решают повесить трубку и пытаются позвонить через один и тот же промежуток времени. Таким образом, и при следующей повторной попытке они оказались в той же ситуации. Это пример активной блокировки, поскольку она может продолжаться вечно.
В качестве общего эталона мы можем сослаться на пример, приведенный в этом видео WWDC, согласно которому система, которая выполняет в 16 раз больше потоков, чем ее ядра ЦП, считается подверженной взрывному росту потоков.
Поскольку Grand Central Dispatch (GCD) не имеет встроенного механизма, предотвращающего взрыв потока, его довольно легко создать с помощью очереди отправки.
Рассмотрим следующий код:
final class HeavyWork {
static func dispatchGlobal(seconds: UInt32) {
DispatchQueue.global(qos: .background).async {
sleep(seconds)
}
}
}
// Execution:
for _ in 1...150 {
HeavyWork.dispatchGlobal(seconds: 3)
}
После выполнения приведенный выше код создаст в общей сложности 150 потоков, что приведет к взрыву потоков. В этом можно убедиться, приостановив выполнение и проверив навигатор отладки.
3.2.1 Multithreading Theme | Back To iOSWiki Contents | 3.2.3 GCD Theme