1. Потокобезопасность. Введение
Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.
Одна из проблем работы с многопоточностью - это потокобезопасность. Как только мы начинаем работать с несколькими потоками, то у нас сразу же может возникать ситуация, когда два потока обратились к одной переменной, несколько потоков пытаются прочитать данные, а какой-то поток записывает.
Потокобезопасность — это свойство программы, которое гарантирует корректное и предсказуемое поведение при одновременном доступе из нескольких потоков. Это означает, что данные остаются в актуальном состоянии и не повреждаются, даже когда несколько потоков пытаются их прочитать или изменить одновременно.
Основные критерии потокобезопасности:¶
-
Синхронизация доступа: Необходимость использовать механизмы синхронизации для того, чтобы гарантировать, что только один поток может изменять данные в любой момент времени.
-
Избежание гонки данных: Гарантия того, что два или более потока не могут одновременно изменять одни и те же данные, что может привести к непредсказуемым результатам. Данная проблема очень часто встречается на крупных проектах и её очень сложно отловить.
-
Согласованность данных: Обеспечение того, чтобы данные оставались в согласованном состоянии даже при одновременном доступе из нескольких потоков.
- Правильное использование примитивов: Использование правильных примитивов синхронизации для обеспечения потокобезопасности без излишней блокировки, которая может привести к снижению производительности.
Потокобезопасность — это критически важный аспект разработки многопоточных приложений, который помогает избежать ошибок, связанных с некорректным доступом к данным.
Actor¶
Для решение проблем с многопоточностью, эпл предложили использовать достаточно старый подход, который был ещё описан в 1973 году. Акторы обеспечивают безопасный доступ к своему состоянию из нескольких потоков, что делает их отличным инструментом для обеспечения потокобезопасности. Actor - это ссылочный тип данных, который не поддерживает наследование
actor Counter {
private(set) var value = Int.zero
func increment() {
value += 1
}
}
/// Все обращения теперь через await
let counter = Counter()
Task {
let currentValue = await counter.value
...
await counter.increment()
}
Возникает вопрос, почему нам необходимо теперь писать await перед обращением к переменным или вызовам функций?
Как устроен Actor внутри?¶
Сейчас мы не будем углубляться в исходники swift. А простыми словами опишем основные моменты работы актор:
Каждый Актор изолирует состояние и изменение этого состояния возможно только через методы актора. Актор позволяет работать с этим состоянием только 1 задаче в единицу времени. Благодаря этому происходит избежание гонки данных. Как только мы вызываем метод актора - происходит то же самое как и с задачей, формируется continuation(состояние), которое отдаётся на исполнение Эктору. Важно понимать, что у эктора есть свой исполнитель (serial Executor) - этот исполнитель умеет выполнять задачи одна за другой, тем самым предоставляя доступ к изолированному состоянию одновременно одному потоку.
ОЧЕНЬ ВАЖНО Serial Executor отличается от Serial Dispatch Queue, потому что актор учитывает приоритет задач, которые у него находятся и если например у него стоят на исполнении 2 задачи с приоритетом medium и приходит ещё одна задача приоритета high - то исполнитель возьмёт в работу задачу c приоритетом high, а уже потом выполнит оставшиеся задачи. Также актор умеет сам поднимать приоритет задач в некоторых случаях, но об этом позже
Эта модель позволяет актору безопасно управлять своим состоянием без необходимости в сложных механизмах синхронизации, таких как мьютексы или семафоры, что упрощает разработку многопоточных приложений. Также Swift может оптимизировать работу экторов, минимизируя переключения контекста, что улучшает производительность.
Actor. Async функции¶
Хочется отдельно отметить работу Actor, когда внутри находятся async функции. Если внутри Actor мы используем async функции в которых вызываем await, то в момент вызова await - актор освобождается и берёт в работу следующую задачу. После того как мы вернёмся в исполнение функции после await, то мы можем увидеть состояние отличное от того, которое было до вызова await.
actor Counter {
private(set) var value = Int.zero
func increment() {
value += 1
}
func sendCounter() async {
print(value) <- Здесь будет одно значение
await ...
print(value) <- Здесь оно может быть уже другим
}
}
Данная проблема называется высокоурвневой гонкой или Actor Reentrancy. Но детальнее об этой проблеме мы поговорим дальше.
Немного практики.¶
Чтобы закрепить и познакомиться с Actor сделаете следуюещее:
- Создайте свой Actor, у которого будет изолированное состояние
- Напишите внутри несколько методов, которые будут как-то взаимодействовать с состоянием (для понятности можно в каждый метод добавить print)
- Попробуйте обратиться к методам эктора - посмотрите как изменяется состояние эктора
- Запустите одновременно несколько задач, исследуйте изменение состояни
- Запустите одновременно несколько задач с высоким и низким приоритетом. Какие задачи будут выполняться быстрее?
(Если не можете придумать пример, можно рассмотреть банковский счет, на который можно зачислить деньги, снять, оплатить)