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

问题描述

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

更具体地说,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. 使用“阅读器”。

    上一点是关于测试异步方法的一般观察。但是,如果您确实有一个异步变异对象的方法,则通常不会直接公开变异属性,而是可以使用“读取器”方法以一般的线程安全方式获取属性值。 (例如,在读写器模式下,您可能会异步更新,但您的读者将等待所有未完成的写入先完成。)

    因此,请考虑使用读写器模式的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")
     }
    

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...