Безопасность параллелизма во время обновления кадров в Swift

У меня есть метод 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, но надеюсь, что это поможет прояснить вопрос.

@Роб, я добавил больше контекста. Это не совсем MRE, но я надеюсь, что это может помочь. Спасибо

bli00 12.06.2024 07:41

Итак, вы просто пытаетесь устранить гонку данных для locationData? То есть render может отображать это locationData, а асинхронные сетевые запросы могут обновляться locationData? Похоже, что простой схемы блокировки будет достаточно.

Rob 12.06.2024 08:37

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

bli00 12.06.2024 08:41

В настоящее время никакие блокировки не являются очень быстрыми, особенно нечестные блокировки. Даже NSLock работает довольно быстро. Читатель-писатель немного медленнее. Последовательная очередь GCD работает еще медленнее. Семафоры даже намного медленнее. Если вас это беспокоит, протестируйте его с помощью оптимизированной (не отладочной) сборки. Но я бы отказался от Swift Atomics или его эквивалента, поскольку они созданы для немного другого сценария. Замки будут для вас достаточно быстрыми.

Rob 12.06.2024 09:29
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вы просто пытаетесь предотвратить гонку данных для 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. Очень полезно :)

bli00 12.06.2024 11:04

Другие вопросы по теме