首页 > 解决方案 > 主线程上的 BlockOperation 的 start()

问题描述

为什么在主线程上调用具有超过 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)
}

标签: swiftblockoperation

解决方案


正如 Andreas 指出的那样,文档警告我们

添加到块操作的块以默认优先级分派到适当的工作队列。块本身不应对其执行环境的配置做出任何假设。

我们操作的线程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)

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

如果我们在 Instruments 中观察这一点,我们可以看到,它addExecutionBlock不仅不一定尊重启动操作的线程,而且也不一定尊重队列的串行性质,因为块是并行运行的:

平行线

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

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

产量:

在此处输入图像描述


推荐阅读