将来自 Swift 的调用同步到基于 C 的线程不安全库

Sync calls from Swift to C based thread-unsafe library

我的 Swift 代码需要调用一些非线程安全的 C 函数。所有调用都必须是:

1) 同步(函数的顺序调用,仅在上一次调用 returned 之后),

2) 在同一线程上。

我尝试创建一个队列,然后从一个函数中访问 C:

let queue = DispatchQueue(label: "com.example.app.thread-1", qos: .userInitiated)

func calc(...) -> Double {
    var result: Double!
    queue.sync {
        result = c_func(...)
    }
    return result
}

这改进了行为,但我仍然遇到崩溃 - 有时,不像以前那么频繁,主要是在从 Xcode 调试时。 关于更好处理的任何想法?

编辑

根据下面的评论,有人可以给出一个通用示例,说明如何使用线程 class 来确保在同一线程上顺序执行吗?

编辑 2

在 C 库周围使用这个包装器时,可以看到一个很好的问题示例: https://github.com/PerfectlySoft/Perfect-PostgreSQL

从单个队列访问时它工作正常。但如果涉及多个调度队列,将开始产生奇怪的错误。

所以我设想了一种单一执行线程的方法,当它被调用时,会阻塞调用者,执行计算,解除阻塞调用者和return结果。对每个连续的来电者重复。

像这样:

thread 1     |              |
--------->   |              | ---->
thread 2     | executor     |      ---->
--------->   | thread       |
thread 3     | -----------> |
--------->   |              |            ---->
...

如果您确实需要确保所有 API 调用必须来自单个线程,您可以使用 Thread class plus some synchronization primitives

例如,下面的 SingleThreadExecutor class 提供了这种想法的一种比较直接的实现:

class SingleThreadExecutor {

    private var thread: Thread!
    private let threadAvailability = DispatchSemaphore(value: 1)

    private var nextBlock: (() -> Void)?
    private let nextBlockPending = DispatchSemaphore(value: 0)
    private let nextBlockDone = DispatchSemaphore(value: 0)

    init(label: String) {
        thread = Thread(block: self.run)
        thread.name = label
        thread.start()
    }

    func sync(block: @escaping () -> Void) {
        threadAvailability.wait()

        nextBlock = block
        nextBlockPending.signal()
        nextBlockDone.wait()
        nextBlock = nil

        threadAvailability.signal()
    }

    private func run() {
        while true {
            nextBlockPending.wait()
            nextBlock!()
            nextBlockDone.signal()
        }
    }
}

确保指定的 确实由单个线程调用的简单测试:

let executor = SingleThreadExecutor(label: "single thread test")
for i in 0..<10 {
    DispatchQueue.global().async {
        executor.sync { print("\(i) @ \(Thread.current.name!)") }
    }
}
Thread.sleep(forTimeInterval: 5) /* Wait for calls to finish. */
0 @ single thread test
1 @ single thread test
2 @ single thread test
3 @ single thread test
4 @ single thread test
5 @ single thread test
6 @ single thread test
7 @ single thread test
8 @ single thread test
9 @ single thread test

最后,将代码中的 DispatchQueue 替换为 SingleThreadExecutor,希望这能解决您的问题——非常奇特! — 问题 ;)

let singleThreadExecutor = SingleThreadExecutor(label: "com.example.app.thread-1")

func calc(...) -> Double {
    var result: Double!
    singleThreadExecutor.sync {
        result = c_func(...)
    }
    return result
}

一个有趣的结果...我将我已经接受的 Paulo Mattos 解决方案的性能与我自己早期的实验进行了基准测试,在这些实验中我使用了一种不太优雅和较低级别的 运行 循环和对象引用方法来实现相同的模式。

基于闭包方法的游乐场: https://gist.github.com/deze333/23d11123f02e65c456d16ffe5621e2ee

运行 循环和引用传递方法的游乐场: https://gist.github.com/deze333/82c0ee3e82fd250097449b1b200b7958

使用闭包:

Invocations processed    : 1000
Invocations duration, sec: 4.95894199609756
Cost per invocation, sec : 0.00495894199609756

使用运行循环并传递对象引用:

Invocations processed    : 1000
Invocations duration, sec: 1.62595099210739
Cost per invocation, sec : 0.00162432666544195

传递闭包比引用传递慢 x3 倍,因为它们是在堆上分配的。这确实证实了一篇优秀的 Mutexes and closure capture in Swift 文章中概述的闭包的性能问题。

经验教训:在需要最大性能时不要过度使用闭包,这在移动开发中很常见。

虽然闭包看起来很漂亮!

编辑:

在 Swift 4 中,通过整个模块优化,情况要好得多。关闭速度很快!