Search code examples
iosswiftcore-bluetooth

CoreBluetooth iOS if data race possible when using WriteWithoutResponse


I want to send a byte stream using my peripheral class. So I'm asking myself if the queue is needed for thread safety or if it's safe to delete it to unlock the flow.

I have this following struct

private struct WriteWithoutResponseContext {
    let data: Data
    let characteristic: Characteristic
} 

And this two attributes

private var writeWithoutResponseContextQueue = Queue<WriteWithoutResponseContext>()
private let writeWithoutResponseContextCachingDispatchQueue = DispatchQueue(label: "thread-safe-unsent-data-caching", attributes: .concurrent)

I'm using the queue with a barrier to make sure everything is blocked until I added it to the queue when canSendWriteWithoutResponse is false.

How do we know that peripheralIsReady is not happening between check of canSendWriteWithoutResponse and enqueueing of context?

   func writeValueWithoutResponse(_ data: Data, for characteristic: Characteristic) {
        if self.cbPeripheral.canSendWriteWithoutResponse {
            self.cbPeripheral.writeValue(data, for: characteristic, type: .withoutResponse)
        } else {
            self.writeWithoutResponseContextCachingDispatchQueue.async(flags: .barrier) {
                let context = WriteWithoutResponseContext(data: data, characteristic: characteristic)
                self.writeWithoutResponseContextQueue.enqueue(context)
            }
        }
    }

    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
        self.writeWithoutResponseContextCachingDispatchQueue.sync {
            if let unsentContext = self.writeWithoutResponseContextQueue.dequeue() {
                self.cbPeripheral.writeValue(unsentContext.data, for: unsentContext.characteristic, type: .withoutResponse)
            }
        }
    }

Solution

  • To be safe you need to specify the Queue your CentralManger is operating on e.g where your central manager dispatches central role events. If you not specifying anything the CentralManager will use the main queue which.

    self.centralManager = CBCentralManager(delegate: self, queue: nil, options: options)
    

    So in my case just creating the CentralManager with a serial queue is fine.

    let serialQueue =  DispatchQueue(label: "com.Stackoverflow.64819549", qos: .userInitiated)
    self.centralManager = CBCentralManager(delegate: self, queue: serialQueue, options: options)
    

    With that I don't need the barrier anymore.