Skip to content

Latest commit

 

History

History
201 lines (129 loc) · 11.3 KB

3.2.2 ProblemsOfMultithreading.md

File metadata and controls

201 lines (129 loc) · 11.3 KB

Проблемы многопоточности

  1. Многопоточность (concurrency) в Swift 3. GCD и Dispatch Queues
  2. Dispatch Queues
  3. Многопоточность: Runloop, Многопоточность в iOS и macOS, Deadlock, Livelock, DispatchGroup, Синхронные и асинхронные задачи, @synchronized, Мьютекс, Семафор
  4. Problem Of Concurrency
  5. Race Conditions and Critical Sections

Как только мы позволяем задачам (tasks) работать параллельно, появляются проблемы, связанные с тем, что разные задачи захотят получить доступ к одним и тем же ресурсам. Основных проблемы три:

Состояние гонки (race condition или critical section access)

О шибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Когда несколько потоков обращаются к одному и тому же куску кода в памяти (с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
  1. Создаем свойство value и последовательную очередь serialQueue

  2. Описываем функцию инкрементирования value

  3. Планируем задачу и сразу же возвращаем управление вызывающей очереди

  4. Имитируем продолжительную работу усыпляя поток и тут же вызываем функцию increment

  5. Выводим в консоль значение переменной value, получаем 0 и вот тут начинается самое интересное. Для полноты картины представьте, что начиная с этого пункта и до конца сниппета, код находится в другой части приложения, а зависимости (value, serialQueue) переданы через DI. То есть вы и понятия не имеете, что через 5 секунд value будет инкрементирован. Мы получаем в консоли значение 0 и для нас это своего рода source of truth.

  6. Передаем в переменную value новое значение

  7. На этот раз инкрементируем синхронно

  8. Снова выводим значение value в консоль. Ожидаем получить 11, но получаем 12.

Попробуем визуализировать пример:

alt text

Чтобы решить нашу, достаточно синхронизировать вызывающую очередь и 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

И снова визуализируем:

alt text

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

Решение проблемы:

  1. Actor;
  2. примитивы мьютекса

Инверсия приоритетов (priority inversion)

Логическое несоответствие с правилами планирования — задача с более высоким приоритетом находится в ожидании в то время как низкоприоритетная задача выполняется;

Низкоприоритетная захватывает ресурс и не отдает его более важной по приоритету задаче и высокоприоритетная ждет выполнения низкоприоритетной задачи.

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

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

В swift возникает, когда очередь вызывает sync внутри самой себя

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

Пример

Первое закрытие не может быть завершено до тех пор, пока не будет завершено второе закрытие:

let serialQueue = DispatchQueue(label: "com.popov.app.exampleQueue")
serialQueue.sync {
    // ...
    serialQueue.sync { // deadlock
        // ...
    }
}

❗ НИКОГДА НЕ вызывайте метод sync на main queue, потому что это приведет к взаимной блокировке (deadlock) вашего приложения!

Замкнутая ситуация: основной поток ждет завершения блока, который не может быть выполнен, потому что основной поток занят ожиданием;

// на главной очереди
DispatchQueue.main.sync { // deadlock
}

Livelock

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

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

Thread Explosion

  1. Understanding Thread Explosion

В качестве общего эталона мы можем сослаться на пример, приведенный в этом видео 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