My Swift code needs to call some C functions that are not thread safe. All calls need to be:
1) synchronous (sequential invocation of function, only after previous call returned),
2) on the same thread.
I've tried to create a queue and then access C from within a function:
let queue = DispatchQueue(label: "com.example.app.thread-1", qos: .userInitiated)
func calc(...) -> Double {
var result: Double!
queue.sync {
result = c_func(...)
}
return result
}
This has improved the behaviour yet I still get crashes - sometimes, not as often as before and mostly while debugging from Xcode. Any ideas about better handling?
Edit
Based on the comments below, can somebody give an general example of how to use a thread class to ensure sequential execution on the same thread?
Edit 2
A good example of the problem can be seen when using this wrapper around C library: https://github.com/PerfectlySoft/Perfect-PostgreSQL
It works fine when accessed from a single queue. But will start producing weird errors if several dispatch queues are involved.
So I am envisaging an approach of a single executor thread, which, when called, would block the caller, perform calculation, unblock the caller and return result. Repeat for each consecutive caller.
Something like this:
thread 1 | |
---------> | | ---->
thread 2 | executor | ---->
---------> | thread |
thread 3 | -----------> |
---------> | | ---->
...
If you really need to ensure that all API calls must come from a single thread, you can do so by using the Thread
class plus some synchronization primitives.
For instance, a somewhat straightforward implementation of such idea is provided by the SingleThreadExecutor
class below:
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()
}
}
}
A simple test to ensure the specified block is really being called by a single thread:
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
Finally, replace DispatchQueue
with SingleThreadExecutor
in your code and let's hope this fixes your — very exotic! — issue ;)
let singleThreadExecutor = SingleThreadExecutor(label: "com.example.app.thread-1")
func calc(...) -> Double {
var result: Double!
singleThreadExecutor.sync {
result = c_func(...)
}
return result
}