У меня есть и приложение, которое имеет Singleton , которая хранит информацию по всему приложению. Однако это создает некоторые проблемы гонки данных при использовании синглтона из разных потоков.

Вот очень фиктивная и упрощенная версия проблемы:

< Сильный > Singleton

class Singleton {
    static var shared = Singleton()

    var foo: String = "foo"
}

<Сильное> Использование синглтона (из AppDelegate для простоты)

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        DispatchQueue.global().async {
            var foo = Singleton.shared.foo // Causes data race
        }

        DispatchQueue.global().async {
            Singleton.shared.foo = "bar"   // Causes data race
        }

        return true
    }
}

Есть ли способ гарантировать, что синглтон является потокобезопасным, чтобы его можно было использовать из любой точки приложения, не беспокоясь о том, в каком потоке вы находитесь?

Этот вопрос <сильный> не дубликат Dispatch_Once Модель Singleton в Swift с момента (если я правильно понял) в тудах они обращаются к проблеме доступа к самому объекту Singleton, но не гарантируя, что чтение и запись его свойств безопасно выполнено.

19
nikano 7 Мар 2018 в 22:50
2
См. stackoverflow.com/questions/ 38373338 /… для примера решения.
 – 
rmaddy
7 Мар 2018 в 22:56
Кажется, это работает отлично. Спасибо, что указали мне правильное направление!
 – 
nikano
7 Мар 2018 в 23:05
1
Вот еще лучшее решение (см. Тот, который использует барьерную синхронизацию для записи): stackoverflow.com/a/44023665/1226963
 – 
rmaddy
7 Мар 2018 в 23:19
 – 
sundance
7 Мар 2018 в 23:46
- Его не беспокоит какая-либо гонка с самим объектом-одиночкой. Его беспокоит синхронизация доступа к свойствам синглтона, это другой вопрос.
 – 
Rob
7 Мар 2018 в 23:50

2 ответа

Лучший ответ

Благодаря комментариям @rmaddy, которые указали мне в правильном направлении, я смог решить проблему.

Чтобы сделать свойство foo потокобезопасного Singleton, его необходимо изменить следующим образом:

    class Singleton {

    static let shared = Singleton()

    private init(){}

    private let internalQueue = DispatchQueue(label: "com.singletioninternal.queue",
                                              qos: .default,
                                              attributes: .concurrent)

    private var _foo: String = "aaa"

    var foo: String {
        get {
            return internalQueue.sync {
                _foo
            }
        }
        set (newState) {
            internalQueue.async(flags: .barrier) {
                self._foo = newState
            }
        }
    }

    func setup(string: String) {
        foo = string
    }
}

Безопасность потоков достигается, имея вычисленное свойство foo, которое использует internalQueue для доступа к свойству «Real» _foo.

Кроме того, для повышения производительности internalQueue создается как параллельный. А это значит, что при записи в свойство нужно добавить флаг barrier.

Что делает флаг {barrier, заключается в том, чтобы гарантировать, что рабочий элемент будет выполнен, когда все ранее запланированные рабочие элементы на очередь закончили.

46
Community 4 Янв 2020 в 23:24
2
Если один и тот же синглтон имеет член foo2, вы защищаете его с помощью той же внутренней очереди или другой?
 – 
ToddX61
12 Июн 2019 в 12:54
1
Я не тестировал его, но мой первый ответ - использовать ту же внутреннюю очередь, поскольку она "параллельна", если должна работать нормально.
 – 
nikano
12 Июн 2019 в 13:15
2
Класс Singleton использует статический экземпляр shared: Singleton, который является одним и тем же во всех экземплярах класса Singleton. Но объект internalQueue отличается в каждом экземпляре Singleton. Как iOS узнает, что блоки, отправленные различным internalQueue объектам, должны быть сериализованы для записи? Документы Apple подразумевают, что label предназначен только для отладки. Разве internalQueue не должно быть static?
 – 
Gene S
14 Авг 2019 в 16:30
2
Почему бы не использовать последовательную очередь, а вместо этого использовать параллельную очередь?
 – 
CyberMew
16 Сен 2019 в 12:54
2
Потому что последовательная очередь не поддерживает несколько чтений одновременно. Поскольку нам не нужно блокировать доступ только к одному чтению за раз, мы используем параллельную очередь, чтобы разрешить несколько чтений. Однако нам нужно поддерживать только одну запись за раз, поэтому мы используем флаг барьера.
 – 
gregkerzhner
7 Янв 2020 в 23:02

Swift Thread Safe Singleton

[GCD]

[Быстрый барьерный флаг для потокобезопасности]

Вы можете реализовать Swift's Singleton Pattern для одновременной связки с использованием GCD и 3 основных вещей:

  1. Настраиваемая параллельная очередь - локальная очередь для повышения производительности, в которой можно выполнять несколько операций чтения одновременно.
  2. sync - customQueue.sync для <{x1}} для <{x1} Общий ресурс - иметь четкое API без обратных вызовов
  3. barrier flag - customQueue.async(flags: .barrier) для операции записи : дождитесь завершения выполняемых операций -> выполнить задачу записи -> продолжить выполнение задачи
public class MySingleton {
    public static let shared = Singleton()
    
    //1. custom queue
    private let customQueue = DispatchQueue(label: "com.mysingleton.queue", qos: .default, attributes: .concurrent)
    //shared resource
    private var sharedResource: String = "Hello World"

    //computed property can be replaced getters/setters
    var computedProperty: String {
        get {
            //2. sync read
            return customQueue.sync {
                sharedResource
            }
        }
        set {
            //3. async write
            customQueue.async(flags: .barrier) {
                sharedResource = newValue
            }
        }
    }
    
    private init() {
    }
}
3
yoAlex5 15 Фев 2022 в 14:48