start () для BlockOperation в основном потоке - PullRequest
3 голосов
/ 19 февраля 2020

Почему вызов 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 Ответ

2 голосов
/ 19 февраля 2020

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

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

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

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

Parallel

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

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

Выход:

enter image description here

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...