首页 > 解决方案 > 如何在 Swift 单元测试中强制队列不在主线程上运行

问题描述

有很多关于如何强制代码在主线程上运行的示例。我对单元测试有相反的需求。我想排除主线程从事一些工作。

为什么我需要这个:我想测试一个特定的函数是否正确运行,特别是当它同时从 2 个或更多后台线程调用时(而主线程是免费且可用的)。这是因为函数本身使用主线程,而它是一个典型的用例,它将从后台线程调用,可能是并发的。

我已经有一个测试用例,其中一个调用线程是主线程。但我也想测试主线程不忙时的情况,当2个或更多其他线程调用该函数时。

问题是似乎没有办法让主线程自由。例如,即使队列是用 定义的qoc: .background,主线程仍然被占用:

private let queue = DispatchQueue(label: "bgqueue", qos: .background, attributes: .concurrent)
let iterations = 10
DispatchQueue.concurrentPerform(iterations: iterations) { _ in

    queue.async { 
        callMyFunction() // still may run on main thread, even with 1-2 iterations
    }
}

我能想到的唯一方法是在同一位置阻塞所有线程(例如使用CountDownLatch),然后从所有线程继续运行,但主要是:

let latch = CountDownLatch(count: iterations)

DispatchQueue.concurrentPerform(iterations: iterations) { _ in

    latch.countdown()
    latch.await()
    if !Thread.isMainThread {
        callMyFunction()
    }
}

这里的问题是 1 - 必须确保iterations < available threads;2 - 感觉阻塞主线程是错误的。

那么有没有更好的解决方案呢?如何在单元测试中排除主线程的工作?DispatchQueue

标签: swiftmultithreading

解决方案


只要您不分派到主队列(或其他使用主队列作为其“目标”的队列),async就永远不会使用主线程。有一些有趣的优化sync会影响使用哪个线程(在此不相关),但不适用于async. 异步调度的任务将在与该 QoS 关联的工作线程上运行,而不是在主线程上运行。

所以,继续在你的测试中插入一个线程断言,以确保它不在主线程上,我想你会发现它很好:

XCTAssertFalse(Thread.isMainThread, "Shouldn't be on main thread")

话虽如此,您提到“函数本身使用主线程”这一事实。如果是这种情况,如果你的测试阻塞了主线程,你很容易死锁,等待被调用的函数也完成主线程上的处理。

您通常可以通过使用“期望”来避免这些类型的单元测试死锁。

func testWillNotDeadlock() throws {
    let expectation = self.expectation(description: "testWillNotDeadlock")
    someAsyncMethod {
        expectation.fulfill()
    }
    waitForExpectations(timeout: 100)
}

在这种情况下,虽然someAsyncMethod可能使用了主线程,但我们并没有死锁,因为我们确保测试使用了预期而不是阻塞当前线程。

因此,最重要的是,如果您正在测试一些必须使用主线程的异步方法,请确保测试不会阻塞,而是使用预期。


诚然,还有其他潜在问题的来源。例如,您调用async100 次迭代。您需要小心这种模式,因为您可以通过这种“线程爆炸”轻松耗尽所有工作线程。如果闭concurrentPerform包中的代码正在调用async.


推荐阅读