Оптимизация блокировки двойной проверки для реализации поточно-ориентированной отложенной загрузки в Swift - PullRequest
0 голосов
/ 10 июня 2018

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

На всякий случай, если вам интересно, это для DI библиотеки , над которой я сейчас работаю.

Код, о котором я говорю, является следующим :

final class Builder<I> {

   private let body: () -> I

   private var instance: I?
   private let instanceLocker = NSLock()

   private var isSet = false
   private let isSetDispatchQueue = DispatchQueue(label: "\(Builder.self)", attributes: .concurrent)

   init(body: @escaping () -> I) {
       self.body = body
   }

   private var syncIsSet: Bool {
       set {
          isSetDispatchQueue.async(flags: .barrier) {
             self.isSet = newValue
          }
       }
       get {
          var isSet = false
          isSetDispatchQueue.sync {
              isSet = self.isSet
          }
          return isSet
       }
   }

   var value: I {

       if syncIsSet {
           return instance! // should never fail
       }

       instanceLocker.lock()

       if syncIsSet {
           instanceLocker.unlock()
           return instance! // should never fail
       }

       let instance = body()
       self.instance = instance

       syncIsSet = true
       instanceLocker.unlock()

       return instance
    }
}

Логика заключается в том, чтобы разрешить одновременное чтение isSet, поэтому доступ к instanceможет быть запущен параллельно из разных потоков.Чтобы избежать условий гонки (в этом я не уверен на 100%), у меня есть два барьера.Один при настройке isSet и один при настройке instance.Хитрость заключается в том, чтобы разблокировать более позднюю версию только после того, как для isSet установлено значение true, поэтому потоки, ожидающие разблокировки instanceLocker, блокируются во второй раз на isSet, пока он асинхронно записывается в очередь одновременной отправки.

Я думаю, что я очень близок к окончательному решению, но так как я не эксперт по распределенным системам, я хотел бы убедиться.

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

Итак, мои два вопроса:

  • Это 100% поточно-ориентированный и если нет, то почему?
  • Есть ли более эффективный способ сделать это в Swift?

1 Ответ

0 голосов
/ 10 июня 2018

IMO, правильный инструмент здесь - os_unfair_lock.Смысл двойной проверки блокировки состоит в том, чтобы избежать затрат на полную блокировку ядра.os_unfair_lock обеспечивает это в неоспоримом случае.«Недобросовестная» часть этого в том, что он не дает обещаний ожидающим потокам.Если один поток разблокируется, то разрешается повторная блокировка без возможности получения другим ожидающим потоком (и, следовательно, может голодать).На практике с очень маленьким критическим разделом это не актуально (в этом случае вы просто проверяете локальную переменную на ноль).Это примитив более низкого уровня, чем отправка в очередь, что очень быстро, но не так быстро, как unsair_lock, так как он опирается на примитивы, такие как unfair_lock.

final class Builder<I> {

    private let body: () -> I
    private var lock = os_unfair_lock()

    init(body: @escaping () -> I) {
        self.body = body
    }

    private var _value: I!
    var value: I {
        os_unfair_lock_lock(&lock)
        if _value == nil {
            _value = body()
        }
        os_unfair_lock_unlock(&lock)

        return _value
    }
}

Обратите внимание, что вы были правы сделатьсинхронизация по syncIsSet.Если бы вы рассматривали его как примитив (как это обычно бывает при других двойных проверках синхронизации), то вы полагались бы на вещи, которые Swift не обещает (как на атомизацию написания Bools, так и на то, что он фактически проверяет логическое значение).дважды, так как нет volatile).Учитывая, что вы делаете синхронизацию, сравнение между os_unfair_lock и отправкой в ​​очередь.

При этом, по моему опыту, такая лень почти всегда неоправданна в мобильных приложениях.Это на самом деле экономит ваше время, только если переменная очень дорогая, но, вероятно, никогда не доступна.Иногда в массово параллельных системах возможность перенести инициализацию имеет смысл, но мобильные приложения работают на довольно ограниченном количестве ядер, поэтому, как правило, не хватает лишних ядер, чтобы скрыть это.Как правило, я бы не стал заниматься этим, если вы уже не обнаружили, что это серьезная проблема, когда ваша платформа используется в живых системах.Если да, то я рекомендую профилировать ваш подход по отношению к os_unfair_lock в реальных ситуациях, которые показывают эту проблему.Я ожидаю, что os_unfair_lock победит.

...