start() для BlockOperation в основном потоке

Почему вызов start() для BlockOperation с более чем 1 блоком в основном потоке не вызывает его блок в основном потоке? Мой первый тест всегда проходит, но второй не каждый раз - иногда блоки выполняются не в основном потоке

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)
}

1 ответ

Решение

Как отметил Андреас, документация предупреждает нас:

Блоки, добавленные в операцию блока, отправляются с приоритетом по умолчанию в соответствующую рабочую очередь. Сами блоки не должны делать никаких предположений о конфигурации среды их выполнения.

Нить, на которой мы start операция, а также maxConcurrentOperationCountповедение очереди управляется на уровне операции, а не отдельными исполнительными блоками внутри операции. Добавление блока к существующей операции - это не то же самое, что добавление новой операции в очередь. Очередь операций определяет отношения между операциями, а не между блоками внутри операции.

Проблема может быть раскрыта, если заставить эти блоки делать что-то, что требует немного времени. Рассмотрим задачу, которая ждет одну секунду (обычно вы никогда неsleep, но мы делаем это просто для имитации медленной задачи и демонстрации рассматриваемого поведения). Я также добавил необходимый код "интересных мест", чтобы мы могли наблюдать за этим в инструментах, что упрощает визуализацию происходящего:

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 просто будет запускаться параллельно в "соответствующей рабочей очереди", не обязательно в текущем потоке.

Если мы посмотрим на это в инструментах, мы увидим, что не только addExecutionBlock не обязательно учитывать поток, в котором была запущена операция, но он также не учитывает последовательный характер очереди с блоками, работающими параллельно:

Очевидно, если вы добавите эти блоки как отдельные операции, то все будет хорошо:

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

Урожайность:

Другие вопросы по тегам