Search code examples
iosswiftmultithreadinggrand-central-dispatchblocking

Sync calls from Swift to C based thread-unsafe library


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     | -----------> |
--------->   |              |            ---->
...

Solution

  • 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
    }