Discover concurrency in SwiftUI
https://developer.apple.com/videos/play/wwdc2021/10019/
Рассмотрим следующий код:
1. Присвоение значения в переменную items, помеченную как
2. Как только SwiftUI видит событие
Именно такая последовательность гарантируется, так как код запускается на
Предположим, что загрузка фото, происходит слишком долго вследствие слабого соединения, и в этом случае мы хотим “разгрузить” главный поток, чтобы как минимум не было фриза у приложения, для этого мы выносим загрузку в глобальную очередь:
В этом случае, возможна следующая ситуация:
3. Срабатывает событие
4. SwiftUI делает snapshot текущего значения items
5. Само присвоение items происходит уже в новом цикле runloop-а (то есть триггер события
6. SwiftUI сравнивает значения и не видит изменения (так как новое еще не успело записаться в новом цикле, а snapshot еще равен текущему значению)
Чтобы SwiftUI видел изменения, нужно чтобы изменения произошли в следующем порядке:
Чтобы гарантировать такой порядок до Swift 5.5 и iOS 15 (ждем поддержки ниже в Xcode 13.2), необходимо было вернуться на
Чтобы гарантировать, что работа происходит на
Связывать UI теперь можно не через
Выполнение
#watchthis #readthis
https://developer.apple.com/videos/play/wwdc2021/10019/
Рассмотрим следующий код:
class Photos: ObservableObject {
@Published var items: [Photos] = []
func updateItems() {
let fetched = fetchPhotos()
items = fetched
}
}
1. Присвоение значения в переменную items, помеченную как
@Published
, триггерит событие objectWillChange
, сразу после этого в storage @Published
записывается новое значение.2. Как только SwiftUI видит событие
objectWillChange
, происходит snapshot текущего значения items, в следующем runloop-е происходит сравнение сделанного snapshot с текущим значением itemsИменно такая последовательность гарантируется, так как код запускается на
Main actor
.Предположим, что загрузка фото, происходит слишком долго вследствие слабого соединения, и в этом случае мы хотим “разгрузить” главный поток, чтобы как минимум не было фриза у приложения, для этого мы выносим загрузку в глобальную очередь:
class Photos: ObservableObject {
@Published var items: [Photos] = []
func updateItems() {
DispatchQueue.global().async {
let fetched = fetchPhotos()
items = fetched
}
}
}
В этом случае, возможна следующая ситуация:
3. Срабатывает событие
objectWillChange
4. SwiftUI делает snapshot текущего значения items
5. Само присвоение items происходит уже в новом цикле runloop-а (то есть триггер события
objectWillChange
и присвоения items происходит в разных циклах)6. SwiftUI сравнивает значения и не видит изменения (так как новое еще не успело записаться в новом цикле, а snapshot еще равен текущему значению)
Чтобы SwiftUI видел изменения, нужно чтобы изменения произошли в следующем порядке:
objectWillChange
- Изменение состояния - Runloop входит в новый циклЧтобы гарантировать такой порядок до Swift 5.5 и iOS 15 (ждем поддержки ниже в Xcode 13.2), необходимо было вернуться на
Main actor
. Начиная с Swift 5.5 и iOS 15 (начиная с Xcode 13.2, ниже), все становится проще и достаточно юзать просто await
, который продолжит выполнение работы на Main actor
после того, как отработает async
. Это называется “to yield main actor”.
class Photos: ObservableObject {
@Published var items: [Photos] = []
func updateItems() async {
let fetched = await fetchPhotos()
items = fetched
}
}
Чтобы гарантировать, что работа происходит на
Main actor
мы добавляем аннотацию @MainActor
в начало описания класса, это означает, что свойства и метода доступны только из Main actor.Связывать UI теперь можно не через
onAppear { Task {…}}
, а через модификатор .task { async {…} }
, доступный с iOS 15.Выполнение
.task {}
привязано к жизненному циклу View и автоматически отменяется, если жизненный цикл View заканчивается.#watchthis #readthis
Apple Developer
Discover concurrency in SwiftUI - WWDC21 - Videos - Apple Developer
Discover how you can use Swift's concurrency features to build even better SwiftUI apps. We'll show you how concurrent workflows interact...
SwiftUI dev
❗️Главное: 1. ❌ Не использовать AnyView, строится неоптимальный код 2. 📖 В SwiftUI два типа identity у View: structural и explicit, в UIKit - pointer identiity 3. 👆🏻 If-else - относится к structural типу. If-else разворачивается «под капотом» в _ConditionalContent…
This media is not supported in your browser
VIEW IN TELEGRAM
Пояснение к п.6:
когда объект подписываем под
#readthis
когда объект подписываем под
Identifiable
, это означает что объект должен иметь что-то уникальное, по чему можно идентифицировать объект. Типичным хаком является использование UUID()
, который относится к числу нестабильных explicit identity. Это означает, что каждый раз получая объект, мы будем получать новый - с другим UUID, это убивает reusability и может вызывать проблемы с памятью помимо проблем с просмотром и анимацией.#readthis
Protect mutable state with Swift actors
https://developer.apple.com/videos/play/wwdc2021/10133/
1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.
Одна из основных проблем многопоточности - data race/ гонка данных (когда минимум два потока пытаются получить доступ к общему ресурсу и минимум один - доступ на запись).
Рассмотрим код:
В разный момент времени print может вернуть 1, 2; 2,1 и даже 2,2 или 1,1 (если обе таски прочитают 0 или 1 в начальном состоянии).
Проблема гонки потоков относится к классу недетерминированных (которые можно получить строго в определенной последовательности), так как системный планировщик может запускать параллельные таски каждый раз по-разному.
Value семантика позволяет избегать гонки потоков как в следующем примере (Словари и массивы в Swift относятся к value типам):
Но при этом, если мы изменим код сверху в value-семантике (выбрав не
гонка потоков все равно остается, так как на счетчик ссылаются из двух параллельных задач. К тому же, компилятор и вовсе выдаст ошибку. Таким образом, необходим инструмент для синхронизации для одновременного доступа к общему изменяемому state из параллельных задач (до этого момента мы могли юзать Atomic, Locks или серийные очереди).
🧐Actors предоставляют такой инструмент синхронизации. У Actor есть свой state, который изолирован от остальной части программы так, что любой доступ к state происходит через actor, а actor из коробки обеспечивает, что никакой другой код параллельно не получит доступ к state (по сути это инструмент и напоминает lock и серийную очередь).
❗️Actors - новый тип в swift, у них те же возможности, что и у обычных name-типов: у них могут быть свойства, методы, конструкторы, subscripts, могут подписываться под протоколы и расширяться через extensions, но не наследоваться!
❗️Actors относятся к reference типам как классы (потому что главное назначение actors - предоставлять доступ к общему ресурсу)
Таким образом, основной отличительной чертой actors является то, что они изолируют данные от остальной части программы и обеспечивают синхронизированный доступ к этим данным.
Изменив, код выше на:
Мы гарантируем, что доступ к value не будет предоставляться параллельно.
Но при этом, если мы запустим следующий код:
То print нам вернет или 1,2 , или 2,1 (так как очередность тасок не гарантируется), но при этом НЕ будет значения 1,1 или 2,2!
В случае же одновременного доступа к state, как мы можем гарантировать, чтобы другая таска ждала, пока выполнится первая? Для этого в swift появился механизм из коробки для этого: запускать через
Так как таска запускается как async, то другая таска пытающаяся получить доступ к actor, видя что он занят, перейдет в режим suspended и CPU будет выполнять другую работу, поток не будет блокирован.
#watchthis #readthis
https://developer.apple.com/videos/play/wwdc2021/10133/
1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.
Одна из основных проблем многопоточности - data race/ гонка данных (когда минимум два потока пытаются получить доступ к общему ресурсу и минимум один - доступ на запись).
Рассмотрим код:
class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
let counter: Counter = .init()
Task.detached {
print(counter.increment())
}
Task.detached {
print(counter.increment())
}
В разный момент времени print может вернуть 1, 2; 2,1 и даже 2,2 или 1,1 (если обе таски прочитают 0 или 1 в начальном состоянии).
Проблема гонки потоков относится к классу недетерминированных (которые можно получить строго в определенной последовательности), так как системный планировщик может запускать параллельные таски каждый раз по-разному.
Value семантика позволяет избегать гонки потоков как в следующем примере (Словари и массивы в Swift относятся к value типам):
var array1 = [1, 2]
var array2 = array1
array1.append(3)
array2.append(4)
print(array1) // [1,2,3]
print(array2) // [1,2,4]
Но при этом, если мы изменим код сверху в value-семантике (выбрав не
class
, а struct
):struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
Var counter: Counter = .init()
Task.detached {
print(counter.increment())
}
Task.detached {
print(counter.increment())
}
гонка потоков все равно остается, так как на счетчик ссылаются из двух параллельных задач. К тому же, компилятор и вовсе выдаст ошибку. Таким образом, необходим инструмент для синхронизации для одновременного доступа к общему изменяемому state из параллельных задач (до этого момента мы могли юзать Atomic, Locks или серийные очереди).
🧐Actors предоставляют такой инструмент синхронизации. У Actor есть свой state, который изолирован от остальной части программы так, что любой доступ к state происходит через actor, а actor из коробки обеспечивает, что никакой другой код параллельно не получит доступ к state (по сути это инструмент и напоминает lock и серийную очередь).
❗️Actors - новый тип в swift, у них те же возможности, что и у обычных name-типов: у них могут быть свойства, методы, конструкторы, subscripts, могут подписываться под протоколы и расширяться через extensions, но не наследоваться!
❗️Actors относятся к reference типам как классы (потому что главное назначение actors - предоставлять доступ к общему ресурсу)
Таким образом, основной отличительной чертой actors является то, что они изолируют данные от остальной части программы и обеспечивают синхронизированный доступ к этим данным.
Изменив, код выше на:
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
Мы гарантируем, что доступ к value не будет предоставляться параллельно.
Но при этом, если мы запустим следующий код:
let counter: Counter = .init()
Task.detached {
print(counter.increment())
}
Task.detached {
print(counter.increment())
}
То print нам вернет или 1,2 , или 2,1 (так как очередность тасок не гарантируется), но при этом НЕ будет значения 1,1 или 2,2!
В случае же одновременного доступа к state, как мы можем гарантировать, чтобы другая таска ждала, пока выполнится первая? Для этого в swift появился механизм из коробки для этого: запускать через
await
.Task.detached {
print(await counter.increment())
}
Task.detached {
print(await counter.increment())
}
Так как таска запускается как async, то другая таска пытающаяся получить доступ к actor, видя что он занят, перейдет в режим suspended и CPU будет выполнять другую работу, поток не будет блокирован.
#watchthis #readthis