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

Posted

技术标签:

【中文标题】将 Swift 调用同步到基于 C 的线程不安全库【英文标题】:Sync calls from Swift to C based thread-unsafe library 【发布时间】:2017-12-09 00:51:40 【问题描述】:

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

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

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 调试时。 关于更好的处理有什么想法吗?

编辑

根据下面的cmets,有人可以举一个通用的例子来说明如何使用线程类来确保在同一个线程上顺序执行吗?

编辑 2

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

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

所以我正在设想一种单一执行线程的方法,当被调用时,它将阻塞调用者,执行计算,解除阻塞调用者并返回结果。对每个连续的调用者重复此操作。

类似这样的:

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

【问题讨论】:

什么是崩溃堆栈跟踪? 崩溃发生在 C 函数中。从技术上讲,这不是崩溃,C 函数只是调用 abort() 并退出,因为它的内部调用堆栈顺序不正确。意思不是预期的“a”->“b”->“c”,而是看到类似“a”->“d”->“e”->“b”->“c”的调用顺序。对我来说,它看起来像是从另一个线程调用 C 函数的 Swift 队列。 使用 DispatchQueue 并不能保证该块每次都在同一个线程上运行。它只保证同步块不会与同一同步队列上的任何其他块同时运行。 GCD 完全有可能决定在单独的线程上运行代码。 @hhanesand 知道了。如果有人有类似的经历,希望有人可以分享一个代码sn-p。有问题的 C 库只是跟踪对其核心函数的调用。因此,如果它的一个函数在另一个 Swift 线程上下文中被调用,调用堆栈顺序就会失控并且库会抱怨。就这么简单。据我所知,它与库本身无关,而与我如何构建 Swift 代码无关。 @JohannesWeiß 我可以看到问题发生在 DispatchQueue 在后台切换线程时(很少)。 C 库似乎不使用线程局部变量,而是在内部保留其状态。我添加了一个 PostgreSQL 包装器的示例,我遇到了类似的问题。是的,这是一个棘手的案例...... 【参考方案1】:

如果您真的需要确保所有 API 调用必须来自单个线程,您可以使用 Thread 类加上 some synchronization primitives 来实现。

例如,下面的SingleThreadExecutor 类提供了这种想法的一个简单的实现:

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

一个简单的测试来确保指定的 block 确实被单个线程调用:

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

【讨论】:

太棒了!我已经在操场上彻底测试了该解决方案,并且效果非常好。可以将此案例添加到“异国情调”手册中:gist.github.com/deze333/23d11123f02e65c456d16ffe5621e2ee @DenisZubkov 很好,很高兴它解决了你的问题!请随意复制并粘贴我的代码,只记得在此处链接以方便参考...我相信您只是在 Gist 中所做的;) 做了一些基准测试,结果证明闭包是如此的性能猪!请看我下面的帖子。那只是为了表演的乐趣。你的回答肯定是完全正确的。【参考方案2】:

一个有趣的结果...我对 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

传递闭包的速度要慢 3 倍,因为它们是在堆上分配的,而不是引用传递。这确实证实了一篇出色的 Mutexes and closure capture in Swift 文章中概述的闭包的性能问题。

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

虽然闭包看起来很漂亮!

编辑:

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

【讨论】:

有趣的分析...但是您是否使用发布模式构建进行所有这些基准测试?请记住,Playgrounds 并不是进行严格基准测试的最佳场所……另外:请务必调整 Swift 编译器优化标志以获得最佳性能。

以上是关于将 Swift 调用同步到基于 C 的线程不安全库的主要内容,如果未能解决你的问题,请参考以下文章

TaskCompletionSource 而不是线程安全库

使用多线程在 GNU C 中使用写入函数是不是安全

Swift - 分派到主队列是不是足以使 var 线程安全?

Mysql 主从复制之半同步复制(基于gtid)

使用 DispatchQueue 实现 Swift 数组同步

Java+线程内部调用实例方法会多线程安全吗?