У меня есть метод render, который вызывается в каждом кадре, чтобы нарисовать что-то на экране с помощью Metal. Он отображает некоторый массив точек, хранящихся внутри класса. Однако иногда мне нужно обновить этот массив точек. Я изо всех сил пытаюсь найти решение, которое могло бы обновлять эти точки безопасным способом параллелизма.
А именно, когда render пытается получить доступ к этому массиву, другой поток может его обновлять, что приводит к сбою. Кроме того, render синхронен и не может блокировать или ждать обновления. Кажется, это часто встречающаяся проблема параллелизма, поэтому я ищу здесь помощи, чтобы узнать, какие существуют решения.
Обобщая проблему: есть некая синхронная функция, которая вызывается очень часто и ей нужен доступ к фрагменту данных, который может быть обновлен другим потоком. У меня есть контроль как над методом render, так и над «другим» потоком, который выполняет обновление. Как я могу предотвратить сбой приложения из-за состояния гонки? (просто не должно произойти сбоя, нет никаких соображений «правильности» данных)
Я предположил, что одним из возможных решений является использование атомики. Я не совсем уверен, как это можно сделать, особенно в Swift (не думаете, что в Swift есть атомика?)
Обновлено:
Я попытаюсь добавить сюда еще немного контекста, но добавить MRE может быть сложно, учитывая, что часть того, что я реализую, спрятана в частном репозитории, к которому у меня нет доступа.
Короче говоря, я пытаюсь реализовать пользовательский слой MapBox, следуя этому примеру. Подробности этой конкретной функции render в примере не имеют значения, поскольку мой метод рендеринга должен делать что-то совершенно другое, но в псевдокоде это будет выглядеть примерно так:
class MyCustomLayer: NSObject, CustomLayerHost {
var locationData: [CLLocation] = []
func render(...) {
// calculate the viewport from the parameters
let viewport = ...
if viewport.fitSomeCondition {
// fetch new location data. This can't block and needs to happen async
fetchLocationDataFromServer(viewport: viewport)
// need to somehow update the locationData
}
// render locationData
}
}
Здесь locationData — это массив долгот и широт, а render — это часть протокола CustomLayerHost из MapBox iOS SDK. Исходный код обработки CustomLayerHost недоступен для публики, но вот объявление заголовка:
/**
* Render the layer. This method is called once per frame.
*
* This method is called if underlying map rendering backend uses Metal graphic API.
*
* @param The `parameters` that define the current camera position.
* @param The MTLCommandBuffer used to render all map layers.
* Use to create new command encoders. Do not submit as there could be map
* layers following this custom layer in style's layer list and those won't get
* to be encoded.
* @param The MTLRenderPassDescriptor that defines rendering map to view drawable.
*
*/
- (void)render:(nonnull MBMCustomLayerRenderParameters *)parameters
mtlCommandBuffer:(nonnull id<MTLCommandBuffer>)mtlCommandBuffer
mtlRenderPassDescriptor:(nonnull MTLRenderPassDescriptor *)mtlRenderPassDescriptor;
Я понимаю, что это не MRE, но надеюсь, что это поможет прояснить вопрос.
Итак, вы просто пытаетесь устранить гонку данных для locationData? То есть render может отображать это locationData, а асинхронные сетевые запросы могут обновляться locationData? Похоже, что простой схемы блокировки будет достаточно.
Мое понимание блокировок привело меня к выводу, что они относительно медленны и нежелательны для высокопроизводительных сценариев, таких как обновление кадров. Но может я ошибаюсь?
В настоящее время никакие блокировки не являются очень быстрыми, особенно нечестные блокировки. Даже NSLock работает довольно быстро. Читатель-писатель немного медленнее. Последовательная очередь GCD работает еще медленнее. Семафоры даже намного медленнее. Если вас это беспокоит, протестируйте его с помощью оптимизированной (не отладочной) сборки. Но я бы отказался от Swift Atomics или его эквивалента, поскольку они созданы для немного другого сценария. Замки будут для вас достаточно быстрыми.





Если вы просто пытаетесь предотвратить гонку данных для locationData, я бы посоветовал добавить некоторую синхронизацию. Например, в высокопроизводительных сценариях мы могли бы склониться к синхронизации на основе блокировки:
import os.lock
class MyCustomLayer: NSObject, CustomLayerHost {
private let lock = OSAllocatedUnfairLock() // or `NSLock` would likely be fine, too
private var _locationData: [CLLocation] = []
// synchronized access to _locationData
private var locationData: [CLLocation] {
get { lock.withLock { _locationData } }
set { lock.withLock { _locationData = newValue } }
}
func render() {
// calculate the viewport from the parameters
let viewport = …
// fetch locations if necessary
if viewport.fitSomeCondition {
fetchLocationDataFromServer()
}
// in the meantime, fetch copy of `locationData`
let locationsToRender = locationData
// now render `locationsToRender`, our local copy
…
}
func fetchLocationDataFromServer() {
let request: URLRequest = …
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
locationData = …
}
task.resume()
}
}
Существует множество шаблонов синхронизации (последовательные очереди GCD, актеры и т. д.), но для чего-то, что должно выполняться так часто, как покадрово, я мог бы склониться к более производительному механизму синхронизации, а именно к несправедливым блокировкам, показанным выше. .
Обратите внимание: мы хотим свести к минимуму количество синхронизаций, поэтому я бы не стал повторно ссылаться на locationData внутри render, а лучше сделать это один раз, присвоив его локальной переменной.
Кроме того, это выходит за рамки вопроса, но хорошенько подумайте fitSomeCondition и избегайте избыточных или одновременных сетевых запросов.
Если бы это того стоило, я мог бы рассмотреть возможность переноса этой синхронизации на основе блокировки в отдельную оболочку свойств:
@propertyWrapper
class Synchronized<T> {
private let lock = OSAllocatedUnfairLock()
private var _wrappedValue: T
var wrappedValue: T {
get { lock.withLock { _wrappedValue } }
set { lock.withLock { _wrappedValue = newValue } }
}
init(wrappedValue: T) {
_wrappedValue = wrappedValue
}
}
И это упрощает MyCustomLayer:
class MyCustomLayer: NSObject, CustomLayerHost {
@Synchronized private var locationData: [CLLocation] = []
func render() {
let viewport = …
if viewport.fitSomeCondition {
fetchLocationDataFromServer()
}
// fetch copy of `locationData`
let locationsToRender = locationData
// now render `locationsToRender`, our local copy
…
}
func fetchLocationDataFromServer() {
let request: URLRequest = …
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
locationData = …
}
task.resume()
}
}
Спасибо. Я не знал о разнице в производительности между OSAllocatedUnfairLock и NSLock. Очень полезно :)
@Роб, я добавил больше контекста. Это не совсем MRE, но я надеюсь, что это может помочь. Спасибо