Попытка понять подсистема асинхронной работы

Я пытаюсь начать с использования Operation в стороннем проекте, а не на основе замкнутых обратных вызовов по всему моему сетевому коду, чтобы помочь устранить вложенные вызовы. Поэтому я немного прочел эту тему и наткнулся на эту реализацию:

open class AsynchronousOperation: Operation {

    // MARK: - Properties

    private let stateQueue = DispatchQueue(label: "asynchronous.operation.state", attributes: .concurrent)

    private var rawState = OperationState.ready

    private dynamic var state: OperationState {
        get {
            return stateQueue.sync(execute: {
                rawState
            })
        }
        set {
            willChangeValue(forKey: "state")
            stateQueue.sync(flags: .barrier, execute: {
                rawState = newValue
            })
            didChangeValue(forKey: "state")
        }
    }

    public final override var isReady: Bool {
        return state == .ready && super.isReady
    }

    public final override var isExecuting: Bool {
        return state == .executing
    }

    public final override var isFinished: Bool {
        return state == .finished
    }

    public final override var isAsynchronous: Bool {
        return true
    }


    // MARK: - NSObject

    private dynamic class func keyPathsForValuesAffectingIsReady() -> Set<String> {
        return ["state"]
    }

    private dynamic class func keyPathsForValuesAffectingIsExecuting() -> Set<String> {
        return ["state"]
    }

    private dynamic class func keyPathsForValuesAffectingIsFinished() -> Set<String> {
        return ["state"]
    }


    // MARK: - Foundation.Operation

    public final override func start() {
        super.start()

        if isCancelled {
            finish()
            return
        }

        state = .executing
        execute()
    }


    // MARK: - Public

    /// Subclasses must implement this to perform their work and they must not call 'super'. The default implementation of this function throws an exception.
    open func execute() {
        fatalError("Subclasses must implement 'execute'.")
    }

    /// Call this function after any work is done or after a call to 'cancel()' to move the operation into a completed state.
    public final func finish() {
        state = .finished
    }
}

@objc private enum OperationState: Int {

    case ready

    case executing

    case finished
}

Есть некоторые детали реализации этого подкласса Operation которые я хотел бы получить некоторую помощь в понимании.

  1. Какова цель свойства stateQueue? Я вижу, что используется get и set из state вычислена собственности, но я не могу найти любую документацию, объясняющую sync:flags:execute и sync:execute методы, которые они используют.

  2. Какова цель трех методов класса в разделе NSObject которые возвращают ["state"]? Я не вижу их где-либо использовать. Я нашел, в NSObject, class func keyPathsForValuesAffectingValue(forKey key: String) → Set<String>, но это не похоже, чтобы помочь мне понять, почему эти методы объявлены.

Ответ 1

Вы сказали:

  1. Какова цель свойства stateQueue? Я вижу, что он используется get и set вычисленного свойства state, но я не могу найти никакой документации, которая объясняет методы sync:flags:execute и sync:execute, которые они используют.

Этот код "синхронизирует" доступ к свойству, чтобы сделать его потокобезопасным. Относительно того, почему вам нужно это сделать, см. документацию Operation, в которой указано:

Multicore Considerations

... Когда вы создаете подкласс NSOperation, вы должны убедиться, что любые переопределенные методы остаются безопасными для вызова из нескольких потоков. Если вы реализуете в своем подклассе пользовательские методы, такие как пользовательские методы доступа к данным, вы также должны убедиться, что эти методы поточно-ориентированы. Таким образом, доступ к любым переменным данных в операции должен быть синхронизирован, чтобы предотвратить потенциальное повреждение данных. Для получения дополнительной информации о синхронизации см. Руководство по программированию потоков.

Что касается точного использования этой параллельной очереди для синхронизации, это называется паттерном "читатель-писатель". Эта базовая концепция паттерна "читатель-писатель" заключается в том, что чтение может происходить одновременно по отношению друг к другу (следовательно, sync, без барьера), но запись никогда не должна выполняться одновременно с любым другим доступом к этому свойству (следовательно, async с барьером). Все это описано в видео WWDC 2012 Асинхронные шаблоны проектирования с блоками, GCD и XPC. Обратите внимание, что хотя это видео обрисовывает в общих чертах базовую концепцию, в нем используется более старый синтаксис dispatch_sync и dispatch_barrier_async, а не синтаксис Swift 3 и более поздних версий, в котором используются только синтаксис sync и async(flags: .barrier).

Вы также спросили:

  1. Какова цель трех методов класса в разделе NSObject, которые возвращают ["state"]? Я не вижу их где-либо использовать. Я обнаружил в NSObject class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String>, но это не помогает мне понять, почему эти методы объявлены.

Это просто методы, которые гарантируют, что изменения свойства state вызывают KVN для свойств isReady, isExecuting и isFinished. KVN этих трех ключей имеет решающее значение для правильного функционирования асинхронных операций. В любом случае этот синтаксис описан в Руководстве по программированию наблюдения значения ключа: регистрация зависимых ключей.

Найденный вами метод keyPathsForValuesAffectingValue связан. Вы можете зарегистрировать зависимые ключи с помощью этого метода или использовать отдельные методы, как показано в исходном фрагменте кода.


Кстати, вот пересмотренная версия предоставленного вами класса AsynchronousOperation, а именно:

  1. Вы не должны звонить super.start(). start документация гласит (выделение добавлено):

    Если вы реализуете параллельную операцию, вы должны переопределить этот метод и использовать его для запуска вашей операции. Ваша пользовательская реализация не должна вызывать super в любое время.

  2. Добавьте @objc, необходимый в Swift 4.

  3. Переименован в execute, чтобы использовать main, что является соглашением для подклассов Operation.

  4. Неуместно объявлять isReady как свойство final. Любой подкласс должен иметь право на дальнейшее совершенствование своей логики isReady (хотя мы, по общему признанию, делаем это редко).

  5. Используйте #keyPath, чтобы сделать код немного более безопасным/надежным.

  6. Вам не нужно делать ручной KVN при использовании свойства dynamic. Ручной вызов willChangeValue и didChangeValue в этом примере не требуется.

  7. Измените finish, чтобы он переходил в состояние .finished, только если он еще не завершен.

Таким образом:

public class AsynchronousOperation: Operation {

    /// State for this operation.

    @objc private enum OperationState: Int {
        case ready
        case executing
        case finished
    }

    /// Concurrent queue for synchronizing access to 'state'.

    private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)

    /// Private backing stored property for 'state'.

    private var _state: OperationState = .ready

    /// The state of the operation

    @objc private dynamic var state: OperationState {
        get { return stateQueue.sync { _state } }
        set { stateQueue.async(flags: .barrier) { self._state = newValue } }
    }

    // MARK: - Various 'Operation' properties

    open         override var isReady:        Bool { return state == .ready && super.isReady }
    public final override var isExecuting:    Bool { return state == .executing }
    public final override var isFinished:     Bool { return state == .finished }
    public final override var isAsynchronous: Bool { return true }

    // KVN for dependent properties

    open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if ["isReady", "isFinished", "isExecuting"].contains(key) {
            return [#keyPath(state)]
        }

        return super.keyPathsForValuesAffectingValue(forKey: key)
    }

    // Start

    public final override func start() {
        if isCancelled {
            state = .finished
            return
        }

        state = .executing

        main()
    }

    /// Subclasses must implement this to perform their work and they must not call 'super'. The default implementation of this function throws an exception.

    open override func main() {
        fatalError("Subclasses must implement 'main'.")
    }

    /// Call this function to finish an operation that is currently executing

    public final func finish() {
        if !isFinished { state = .finished }
    }
}

Ответ 2

При использовании обновленного фрагмента кода из ответа Роба следует помнить о возможности ошибки, вызванной этим изменением:

  1. Измените отделку так, чтобы он переходил в состояние .finished только в случае isExecuting.

Вышесказанное противоречит документам Apple:

Помимо простого выхода при отмене операции, также важно переместить отмененную операцию в соответствующее конечное состояние. В частности, если вы сами управляете значениями для завершенных и выполняющихся свойств (возможно, потому что вы реализуете параллельную операцию), вы должны соответствующим образом обновить эти свойства. В частности, вы должны изменить значение, возвращаемое Finished, на YES, а значение, возвращаемое путем выполнения на NO. Вы должны внести эти изменения, даже если операция была отменена до ее начала.

Это приведет к ошибке в нескольких случаях. Например, если Очередь операций с "maxConcurrentOperationCount = 1" получает 3 асинхронные операции AB и C, то, если все операции отменяются во время A, C не будет выполняться, и очередь будет зависать в операции B.

Ответ 3

О вашем первом вопросе: stateQueue блокирует вашу операцию при записи нового значения в ваше рабочее состояние:

    return stateQueue.sync(execute: {
            rawState
    })

и

    stateQueue.sync(flags: .barrier, execute: {
        rawState = newValue
    })

так как ваша операция асинхронна, поэтому перед чтением или записью одного состояния может быть вызвано другое состояние. Как вы хотите написать isExecution, но в то же время isFinished уже вызывается. Поэтому, чтобы избежать этого сценария, stateQueue блокирует состояние операции, которое должно быть прочитано и записано, пока не завершит свой предыдущий вызов. Его работа, как Atomic. Скорее используйте диспетчерскую очередь, вы можете использовать расширение для NSLock, чтобы упростить выполнение критического кода из кода примера Advanced NSOperations в WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/ из https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip, и вы можете реализовать следующее:

private let stateLock = NSLock()

private dynamic var state: OperationState {
    get {
        return stateLock.withCriticalScope{ rawState } 
    }
    set {
        willChangeValue(forKey: "state")

        stateLock.withCriticalScope { 
            rawState = newValue
        }
        didChangeValue(forKey: "state")
    }
}

О вашем втором вопросе: его уведомление KVO для свойства только для чтения isReady, isExecuting, isFinished для управления состоянием операции. Вы можете прочитать это: http://nshipster.com/key-value-observing до конца, чтобы лучше понять KVO.