将来自 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 中,通过整个模块优化,情况要好得多。关闭速度很快!
我的 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 中,通过整个模块优化,情况要好得多。关闭速度很快!