Why is calling start() for BlockOperation with more then 1 block on a main thread not calling its block on the main thread? My first test is always passed but second not every time - some times blocks executes not on the main thread
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()
}
Even next code is failed
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)
}
As Andreas pointed out, the documentation warns us:
The thread on which we
start
the operation, as well as themaxConcurrentOperationCount
behavior of the queue, is managed at the operation level, not at the individual execution blocks within an operation. Adding a block to an existing operation is not the same as adding a new operation to the queue. The operation queue governs the relationship between operations, not between the blocks within an operation.The problem can be laid bare by making these blocks do something that takes a little time. Consider a task that waits one second (you would generally never
sleep
, but we're doing this simply to simulate a slow task and to manifest the behavior in question). I've also added the necessary “points of interest” code so we can watch this in Instruments, which makes it easier to visualize what’s going on:Then use
addExecutionBlock
:Now, I'm adding this to a serial operation queue (because you’d never add a blocking operation to the main queue ... we need to keep that queue free and responsive), but you see the same behavior if you manually
start
this on theOperationQueue.main
. So, bottom line, whilestart
will run the operation “immediately in the current thread”, any blocks you add withaddExecutionBlock
will just run, in parallel, on “an appropriate work queue”, not necessary the current thread.If we watch this in Instruments, we can see that not only does
addExecutionBlock
not necessarily honor the thread on which the operation was started, but it doesn’t honor the serial nature of the queue, either, with the blocks running in parallel:Obviously, if you add these blocks as individual operations, then everything is fine:
Yielding: