在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,当你处理异步方法时,你会:
使用完成处理程序。
通常,如果您有一个异步方法,为了推断对象的状态(例如,何时关闭微调器让用户知道它何时完成),您需要提供一个完成处理程序。 (这是假设简单的 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")
}
使用“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")
}
但是如果没有某种同步读取的方法,您通常也不会有带有异步写入的 属性。如果仅从主线程使用,问题中的示例将起作用。否则,你会有竞争条件。
这段代码为什么会这样执行?请注意测试代码中的注释,它指示哪些行通过和失败。
更具体地说,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,当你处理异步方法时,你会:
使用完成处理程序。
通常,如果您有一个异步方法,为了推断对象的状态(例如,何时关闭微调器让用户知道它何时完成),您需要提供一个完成处理程序。 (这是假设简单的
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") }
使用“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") }
但是如果没有某种同步读取的方法,您通常也不会有带有异步写入的 属性。如果仅从主线程使用,问题中的示例将起作用。否则,你会有竞争条件。