【问题标题】:Trying to Understand Asynchronous Operation Subclass试图理解异步操作子类
【发布时间】:2017-09-19 13:06:10
【问题描述】:

我正在尝试在一个辅助项目中开始使用Operations,而不是在我的网络代码中散布基于闭包的回调来帮助消除嵌套调用。所以我在阅读这个主题时,遇到了this 实现:

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 属性的用途是什么?我看到它被state 计算属性的getset 使用,但我找不到任何解释他们使用的sync:flags:executesync:execute 方法的文档。

    李>
  2. NSObject 部分中返回["state"] 的三个类方法的用途是什么?我没有看到它们在任何地方使用。我在NSObjectclass func keyPathsForValuesAffectingValue(forKey key: String) -&gt; Set&lt;String&gt; 中找到了,但这似乎并不能帮助我理解为什么要声明这些方法。

【问题讨论】:

  • 仅供参考,这是一个相当严重的错误。 start documentation 说(添加了重点):“如果您正在实现并发操作,则必须重写此方法并使用它来启动您的操作。您的自定义实现不得在任何时候调用 super。”
  • @Rob 引用段落中的关键字是“并发”。这仅与不在操作队列中运行的操作相关。请参阅 _isAsynchronous 属性和本段:developer.apple.com/documentation/foundation/operation#1661231
  • @Rob 没有拍摄,但我邀请你仔细看看。我多年来一直在使用操作。甚至 Apple 在著名的 Advanced Operations WWDC2015 谈话中的示例代码也会在 start() 中覆盖并调用 super。我并不是说示例代码是完美的,但是。我的印象是说你不应该叫 super 的段落很容易被误解。
  • 此外,Apple 称之为异步的操作是从 start() 生成一个新线程的操作。这与在后台队列中运行操作不同。
  • 大声笑。我知道“异步操作”是什么意思。最重要的是,关于start 实现的文档再清楚不过了,他们多次重复警告:“在您的start() 方法中,您永远不应该调用super”和“您的自定义实现[@987654342 @] 任何时候都不能打电话给super。”而且我不会将“高级 NSOperations”作为示例,因为正如您所暗示的那样,它的问题是臭名昭著和多方面的。但是,嘿,如果你想打电话给super,把自己打晕。但是应该预先警告未来的读者,因为如果您不小心,可能会导致问题。

标签: swift nsoperation


【解决方案1】:

你说:

  1. stateQueue 属性的用途是什么?我看到它被 state 计算属性的 get 和 set 使用,但我找不到任何解释他们使用的 sync:flags:executesync:execute 方法的文档。

此代码“同步”对属性的访问以使其成为线程安全的。关于为什么需要这样做,请参阅 the Operation documentation,它建议:

多核注意事项

...当您继承NSOperation 时,您必须确保任何被覆盖的方法都可以安全地从多个线程调用。如果您在子类中实现自定义方法,例如自定义数据访问器,您还必须确保这些方法是线程安全的。因此,必须同步访问操作中的任何数据变量,以防止潜在的数据损坏。有关同步的更多信息,请参阅Threading Programming Guide

关于同步使用这个并发队列的确切用途,这被称为“读取器-写入器”模式。读写器模式的这个基本概念是读取可以相互并发(因此sync,没有障碍),但写入绝对不能与该属性的任何其他访问同时执行(因此@987654336 @ 带屏障)。

例如,您可以像这样在数组上实现线程安全的读写器:

class ThreadSafeArray<T> {
    private var values: [T]
    private let queue = DispatchQueue(label: "...", attributes: .concurrent)
    
    init(_ values: [T]) {
        self.values = values
    }
    
    func reader<U>(block: () throws -> U) rethrows -> U {
        return try queue.sync {
            try block()
        }
    }
    
    func writer(block: @escaping (inout [T]) -> Void) {
        queue.async(flags: .barrier) {
            block(&self.values)
        }
    }
    
    // e.g. you might use `reader` and `writer` like the following:
    
    subscript(_ index: Int) -> T {
        get { reader { values[index] } }
        set { writer { $0[index] = newValue } }
    }
    
    func append(_ value: T) {
        writer { $0.append(value) }
    }
    
    func remove(at index: Int) {
        writer { $0.remove(at: index)}
    }
}

显然,在这个Operation 子类中使用reader-writer 更加简单,但上面说明了这种模式。

你还问过:

  1. NSObject 部分中返回["state"] 的三个类方法的用途是什么?我没有看到它们在任何地方使用。我在NSObjectclass func keyPathsForValuesAffectingValue(forKey key: String) -&gt; Set&lt;String&gt; 中找到了,但这似乎并不能帮助我理解为什么要声明这些方法。

这些只是确保对state 属性的更改触发属性isReadyisExecutingisFinished 的KVO 通知的方法。这三个键的 KVO 通知对于异步操作的正确运行至关重要。不管怎样,Key-Value Observing Programming Guide: Registering Dependent Keys 中概述了这种语法。

您找到的keyPathsForValuesAffectingValue 方法是相关的。您可以使用该方法注册相关键,也可以使用原始代码 sn-p 中显示的各个方法。


顺便说一句,这是您提供的AsynchronousOperation 类的修订版,即:

  1. 您不得致电super.start()。正如start documentation 所说(强调):

    如果您要实现并发操作,则必须重写此方法并使用它来启动您的操作。 您的自定义实现在任何时候都不得调用super

  2. 在 Swift 4 中添加 @objc

  3. 重命名execute 以使用main,这是Operation 子类的约定。

  4. isReady 声明为final 属性是不合适的。任何子类都应该有权进一步完善其isReady 逻辑(尽管我们承认很少这样做)。

  5. 使用#keyPath 使代码更安全/更健壮。

  6. 使用dynamic 属性时,您无需手动进行 KVO 通知。本例不需要手动调用willChangeValuedidChangeValue

  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 }
    }
}

【讨论】:

  • 不错的答案,除了基于文档之外,对于异步操作,应该覆盖start 而不是mainmain 用于同步操作)。恕我直言,如果您可以删除 main 覆盖,这将有助于避免混淆
  • 我不同意。 Concurrency Programming Guide 鼓励同时使用 startmain 进行异步操作。在他们关于 main 与异步操作的讨论中,他们说:“虽然您可以在 start 方法中执行任务,但使用 [main] 实现任务可以更清晰地分离您的设置和任务代码。”
  • @vadian - 同意。正如我在回答中所说,我将execute 重命名为main 仅仅是因为这是一个惯例问题;我没有这样做,因为我认为一个不得不。当为此目的使用main 非常普遍(并且Apple 明确考虑)时,引入一个完全不同的方法名称让我感到非常奇怪。我上面的评论只是针对 user1046037 的假设,即 main 用于同步操作。我想知道类似的误解是否促使 calebd 决定首先使用execute...
  • @VinGazoil - 它代表“reader-writer”,一种使用并发队列的同步模式,允许并发读取,但使用屏障进行写入以确保写入正确同步。
  • @Isuru - 也许您可以发布到 gist.github.com 之类的地方,我可以在其中发布 cmets/更正?在回答您的问题时,操作队列将为您拨打start。但是您的main 实现不太正确...
【解决方案2】:

当使用来自Rob's answer 的更新代码 sn-p 时,应注意此更改引起的错误的可能性:

  1. 更改完成,使其仅在 isExecuting 时移动到 .finished 状态。

以上内容违背苹果docs

除了在操作被取消时简单地退出之外,将已取消的操作移动到适当的最终状态也很重要。具体来说,如果您自己管理已完成和正在执行的属性的值(可能是因为您正在实现并发操作),则必须相应地更新这些属性。具体来说,您必须将finished返回的值更改为YES,将execution返回的值更改为NO。即使操作在开始执行之前被取消,您也必须进行这些更改。

这在少数情况下会导致错误。例如,如果“maxConcurrentOperationCount = 1”的Operation Queue得到3个异步操作A B和C,那么如果在A期间取消所有操作,C将不会被执行,队列会卡在操作B上。

【讨论】:

  • 那么这里的解决方案是什么?
  • 好收获。我已经在我的回答中解决了这个问题。谢谢。
【解决方案3】:

关于您的第一个问题:stateQueue 在向您的操作状态写入新值时锁定您的操作:

    return stateQueue.sync(execute: {
            rawState
    })

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

由于您的操作是异步的,因此在读取或写入一个状态之前可以调用另一个状态。就像你想写 isExecution 但同时 isFinished 已经调用了。因此,为了避免这种情况,stateQueue 会锁定要读取和写入的操作状态,直到它完成之前的调用。它的工作就像原子。而是使用调度队列,您可以使用 NSLock 的扩展来简化从 WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip 中的高级 NSOperations 示例代码执行关键代码,您可以像下面这样实现:

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")
    }
}

关于你的第二个问题:它是只读属性 isReady、isExecuting、isFinished 的 KVO 通知,用于管理操作状态。您可以阅读以下内容:http://nshipster.com/key-value-observing 发帖到最后,以便更好地了解 KVO。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-16
    • 2020-01-17
    • 2023-03-16
    相关资源
    最近更新 更多