主线程上的 BlockOperation 的 start()

start() for BlockOperation on the main thread

为什么在主线程上为超过 1 个块的 BlockOperation 调用 start() 而不是在主线程上调用它的块? 我的第一个测试总是通过,但第二个不是每次都通过 - 有时块不在主线程上执行

func test_callStartOnMainThread_executeOneBlockOnMainThread() {
    let blockOper = BlockOperation {
        XCTAssertTrue(Thread.isMainThread, "Expect first block was executed on Main Thread")
    }
    blockOper.start()
}
func test_callStartOnMainThread_executeTwoBlockOnMainThread() {
    let blockOper = BlockOperation {
        XCTAssertTrue(Thread.isMainThread, "Expect first block was executed on Main Thread")
    }
    blockOper.addExecutionBlock {
        XCTAssertTrue(Thread.isMainThread, "Expect second block was executed on Main Thread")
    }
    blockOper.start()
}

连下一个代码都失败了

func test_callStartOnMainThread_executeTwoBlockOnMainThread() {
    let asyncExpectation = expectation(description: "Async block executed")
    asyncExpectation.expectedFulfillmentCount = 2
    let blockOper = BlockOperation {
        XCTAssertTrue(Thread.isMainThread, "Expect first block was executed on Main Thread")
        asyncExpectation.fulfill()
    }
    blockOper.addExecutionBlock {
        XCTAssertTrue(Thread.isMainThread, "Expect second block was executed on Main Thread")
        asyncExpectation.fulfill()
    }
    OperationQueue.main.addOperation(blockOper)
    wait(for: [asyncExpectation], timeout: 2.0)
}

正如安德烈亚斯指出的那样,the documentation warns us:

Blocks added to a block operation are dispatched with default priority to an appropriate work queue. The blocks themselves should not make any assumptions about the configuration of their execution environment.

我们 start 操作所在的线程以及队列的 maxConcurrentOperationCount 行为在操作级别进行管理,而不是在操作中的各个执行块中进行管理。向现有操作添加块与向队列添加新操作不同。操作队列管理操作之间的关系,而不是操作中块之间的关系。

可以通过让这些块做一些需要一点时间的事情来暴露问题。考虑一个等待一秒钟的任务(您通常永远不会 sleep,但我们这样做只是为了模拟一个缓慢的任务并显示有问题的行为)。我还添加了必要的“兴趣点”代码,以便我们可以在 Instruments 中观看,这样可以更轻松地可视化正在发生的事情:

import os.log
let pointsOfInterest = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: .pointsOfInterest)

func someTask(_ message: String) {
    let id = OSSignpostID(log: pointsOfInterest)
    os_signpost(.begin, log: pointsOfInterest, name: "Block", signpostID: id, "Starting %{public}@", message)
    Thread.sleep(forTimeInterval: 1)
    os_signpost(.end, log: pointsOfInterest, name: "Block", signpostID: id, "Finishing %{public}@", message)
}

然后使用addExecutionBlock:

let queue = OperationQueue()          // you get same behavior if you replace these two lines with `let queue = OperationQueue.main`
queue.maxConcurrentOperationCount = 1

let operation = BlockOperation {
    self.someTask("main block")
}
operation.addExecutionBlock {
    self.someTask("add block 1")
}
operation.addExecutionBlock {
    self.someTask("add block 2")
}
queue.addOperation(operation)

现在,我将其添加到串行操作队列(因为您永远不会向主队列添加阻塞操作......我们需要保持该队列的空闲和响应),但您看到的是相同的如果您在 OperationQueue.main 上手动 start 此行为。因此,最重要的是,虽然 start 将 运行 操作“立即在当前线程中”,但您使用 addExecutionBlock 添加的任何块将只是 运行,并行地,在“适当的工作队列”,不需要当前线程。

如果我们在 Instruments 中观察这个,我们可以看到 addExecutionBlock 不仅不一定遵循启动操作的线程,而且也不遵循队列的串行性质,或者, 块 运行ning 并行:

显然,如果您将这些块添加为单独的操作,那么一切都很好:

for i in 1 ... 3 {
    let operation = BlockOperation {
        self.someTask("main block\(i)")
    }
    queue.addOperation(operation)
}

产量: