在Apple的Foundation/Swift/Objective-C中,runLoop.run如何阻塞,但仍然允许DispatchWorkItems处理?

In Apple's Foundation/Swift/Objective-C, how does runLoop.run block, but still allow DispatchWorkItems to process?

这段代码为什么会这样执行?请注意测试代码中的注释,它指示哪些行通过和失败。

更具体地说,RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01)) 如何在那里等待,同时仍允许 DispatchWorkItem{ [weak self] in self?.name = newName } 进行处理?如果线程正在等待 运行 循环,线程如何处理任何工作项?

(或者如果问题没有意义请纠正我的理解)

class Person {
    private(set) var name: String = ""

    func updateName(to newName: String) {
        DispatchQueue.main.async { [weak self] in self?.name = newName }
    }
}

class PersonTests: XCTestCase {
    func testUpdateName() {
        let sut = Person()
        sut.updateName(to: "Bob")
        XCTAssertEqual(sut.name, "Bob") // Fails: `sut.name` is still `""`
        assertEventually { sut.name == "Bob" } // Passes
    }
}

func assertEventually(
    timeout: TimeInterval = 1,
    assertion: () -> Bool
) {
    let timeoutDate = Date(timeIntervalSinceNow: timeout)
    while Date() < timeoutDate {
        RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
        if assertion() == true { return }
    }
    XCTFail()
}

while 循环阻止执行继续,但 run 命令不只是等待,而是处理该线程的 运行 循环上的事件,包括处理GCD 源、定时器、调度块等


FWIW,当你处理异步方法时,你会:

  1. 使用完成处理程序。

    通常,如果您有一个异步方法,为了推断对象的状态(例如,何时关闭微调器让用户知道它何时完成),您需要提供一个完成处理程序。 (这是假设简单的 async 是一些更复杂的异步模式的简化。)

    如果您真的想要一个异步改变对象的异步方法,并且您的应用程序当前不需要知道它何时完成,那么将该完成处理程序设为可选:

     func updateName(to name: String, completion: (() -> Void)? = nil) {
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
             self?.name = name
             completion?()
         }
     }
    

    然后您可以在单元测试中使用期望,这是测试异步方法的标准方法:

     func testUpdateName() {
         let e = expectation(description: "Person.updateName")
    
         let person = Person()
         person.updateName(to: "Bob") {
             e.fulfill()
         }
    
         waitForExpectations(timeout: 1)
    
         XCTAssertEqual(person.name, "Bob")
     }
    
  2. 使用“reader”。

    前一点是关于测试异步方法的一般观察。但是如果你真的有一个异步改变对象的方法,你通常不会直接公开变异属性,而是你可以使用“reader”方法来获取 属性 值,thread-safe方式。 (例如,在 reader-writer 模式中,您可能会异步更新,但您的 reader 会等待任何待处理的写入先完成。)

    因此,考虑使用 reader-writer 模式的 Person

     class Person {
         // don't expose the name at all
         private var name: String = ""
    
         // private synchronization reader-writer queue
         private let queue = DispatchQueue(label: "person.readerwriter", attributes: .concurrent)
    
         // perform writes asynchronously with a barrier
         func writeName(to name: String) {
             queue.async(flags: .barrier) {
                 self.name = name
             }
         }
    
         // perform reads synchronously (concurrently with respect to other reads, but synchronized with any writes)
         func readName() -> String {
             return queue.sync {
                 return name
             }
         }
     }
    

    然后测试将使用 readName

     func testUpdateName() {
         let person = Person()
         person.writeName(to: "Bob")
         let name = person.readName()
         XCTAssertEqual(name, "Bob")
     }
    

    但是如果没有某种同步读取的方法,您通常也不会有带有异步写入的 属性。如果仅从主线程使用,问题中的示例将起作用。否则,你会有竞争条件。