Pawel Klapuch
Pawel Klapuch

Reputation: 490

NSStream (BLE L2CAP) on background thread

I'm looking for some verification of what i'm doing. Also, there's a lot of related topics here and there but none is complete / addresses all challenges.

Overview / Requirements:

Expectations:


So, after much research and prototyping:

thread = Thread(block: { [weak self] in
        let runLoop = RunLoop.current
        let timer = Timer(timeInterval: 0.01, repeats: true, block: { [weak self] _ in self?.run() })
        
        inputStream.schedule(in: runLoop, forMode: .default)
        outputStream.schedule(in: runLoop, forMode: .default)

        CFRunLoopAddTimer(runLoop.getCFRunLoop(), timer, .defaultMode)
        CFRunLoopRun()
    })

And exit:

    timer?.invalidate()
    CFRunLoopRemoveTimer(runLoop.getCFRunLoop(), timer, .defaultMode)
    CFRunLoopStop(runLoop.getCFRunLoop())
    thread?.cancel()

This works - thread/RunLoop runs / stream(s) receive events and when it's time to stop, RunLoop ends / thread is cancelled. My concerns / questions regarding RunLoop and custom Thread(s):

  1. I've seen many sources just using RunLoop.main to schedule NSStream(s). That seems incorrect and unnecessary. But I don't have much knowledge to support it (it works pretty much the same whether I use main or background, but I'd really prefer to use background to avoid weird intermittent behaviors).

  2. Obviously creating threads is not ideal (since we have GCD) but it's required by NSStream API. I've seen sources using DispatchQueue.global() to access CurrentRunLoop but apparently GCD has a limited number of worker threads and such practice is not recommended.

  3. Is it okay to schedule both streams in the same RunLoop? Or is one thread per stream better? Or could I schedule 3 peripherals (6x streams) on a single thread?

  4. Timer - Is there any recommendation as to how often the timer should tick? 10ms for BLE communications seems fine but is it? As far as I see the CPU usage is 1% when I run this code so that's not an issue. But also running it at 1ms would be an overkill? Is there better way (not using Timer)?

  5. There is probably a hard limit as to how many threads I can spawn like this (before they are re-used). I don't think i'll ever have more than handful connected peripherals (also limited by BLE hardware) but still. On the other hand, in a unit-test I can spawn 1000 threads in this way (simultaneously), and the code runs fine on simulator. Is this normal?

Upvotes: 1

Views: 145

Answers (1)

DrMickeyLauer
DrMickeyLauer

Reputation: 4684

This is quite a broad question, but here's some answers based on my experience with custom BLE hardware:

I've seen many sources just using RunLoop.main to schedule NSStream(s). That seems incorrect and unnecessary. But I don't have much knowledge to support it (it works pretty much the same whether I use main or background, but I'd really prefer to use background to avoid weird intermittent behaviors).

I guess this is being done, because correctly handling a background thread with a secondary RunLoop is more involved. Depending on the load on your stream, handling events in the main loop often is "ok" – for the overall performance it's of course better to do it in a dedicated thread.

Obviously creating threads is not ideal (since we have GCD) but it's required by NSStream API. I've seen sources using DispatchQueue.global() to access CurrentRunLoop but apparently GCD has a limited number of worker threads and such practice is not recommended.

A few long-running threads are no problem at all.

Is it okay to schedule both streams in the same RunLoop? Or is one thread per stream better? Or could I schedule 3 peripherals (6x streams) on a single thread?

This depends on your latency requirements. I use one thread for every dedicated peripheral, but schedule both the input and the output stream to that thread's RunLoop.

Timer - Is there any recommendation as to how often the timer should tick? 10ms for BLE communications seems fine but is it? As far as I see the CPU usage is 1% when I run this code so that's not an issue. But also running it at 1ms would be an overkill? Is there better way (not using Timer)?

I'm not sure what you are using the timer for. Just fire up the RunLoop and let the streams notify you via the delegate.

There is probably a hard limit as to how many threads I can spawn like this (before they are re-used). I don't think i'll ever have more than handful connected peripherals (also limited by BLE hardware) but still. On the other hand, in a unit-test I can spawn 1000 threads in this way (simultaneously), and the code runs fine on simulator. Is this normal?

It depends on what you are doing in these threads, but in general: Yes. A desktop class system handles many threads without any problems. From a certain number onwards, you'll notice a performance impact though as the system wastes more and more time with scheduling.

Upvotes: 0

Related Questions