Why does subscribe(on:) a concurrent background queue crash when subscription is deallocated?

I've been trying to ascertain the root cause of some test failures in our application at work and I'm wondering if it's related to inappropriate use of subscribe(on:). We use this operator to push GraphQL request construction to a background queue (DispatchQueue.global()) as the construction can be potentially expensive on the main thread.

Something akin to this code:

    .subscribe(on: DispatchQueue.global())

We haven't seen any issues in our application but have experienced sporadic test crashes/failures. After further investigation, I found that if we don't "properly" store the subscription form a sink, we can experience a EXC_BAD_ACCESS exception. (The task is stored in local scope, but deallocates after a 0.1-second timeout using XCTExpecations)

Upon further research, I've found that the following snippet of code crashes in a playground:

var x = 0
let count = 100
for i in 0..<count {
        .subscribe(on: DispatchQueue.global())
        .sink { value in
            print("sink \(value)")
            x += 1

            if x == count {
                print("finished \(count)")
// note: subscription is not stored.
// The above code eventually crashes with `EXC_BAD_ACCESS`

PlaygroundPage.current.needsIndefiniteExecution = true

Note: It only crashes intermittently. The crash can be produced more reliably if you bump the count past 1000.

My question is: Is this expected if we do not retain the subscription returned from sink? Does anyone have insight into why we might be seeing crashes with this combine pipeline?


The answer is GCD is most unpredictable part of process. I have similar errors while rewriting values from queues.

It appears that the cancel() function for Just's Subscription type is not thread-safe. (Same for Result.Publisher and Optional.Publisher). I have reported one particular way to crash it in FB7722681. You should also report yours.

1 Like