Leaky abstractions in Swift with DispatchQueue

At Q42 we have some apps with very occasional weird multi-threading issues. After a bunch of debugging Mathijs Kadijk and I figured out it had something to do with DispatchSpecificKey. This post details what we found out.

Pop quiz! What do you think this prints?


while true {
  let key = DispatchSpecificKey<Int>()

  if let value = DispatchQueue.main.getSpecific(key: key) {
    print("ERR \(value)")
    exit(1)
  }
  else {
    DispatchQueue.main.setSpecific(key: key, value: 42)
    print("OK")
    sleep(1)
  }
}

Answer: This does not print a infinite stream of OKs every second, as I would have thought. Instead, it stops after three iterations of the loop:


OK
OK
ERR 42
Program ended with exit code: 1

The output changes a bit each time I run the program. Sometimes it makes it to three or four OKs, but often it only prints one OK and then stops with an ERR.

My question after seeing this behaviour: Wut? How can two distinct keys result in the same value?!?

Wrappers around C APIs

It turns out these (woefully underdocumented) Swift APIs are wrappers around C APIs. The code for this is all open source, see Queue.swift. The underlying C APIs are better documented and give some insight into how this all works internally:

The documentation for dispatch_queue_set_specific explains how the key is just a pointer to something, it doesn’t really matter what it’s pointing to.

Keys are only compared as pointers and are never dereferenced. Thus, you can use a pointer to a static variable for a specific subsystem or any other value that allows you to identify the value uniquely.

For this reason, the Swift implementation of DispatchSpecificKey is very simple:


public final class DispatchSpecificKey<T> {
	public init() {}
}

The initialised object is only used to get a pointer value that can be passed to the dispatch_queue_set_specific function. This pointer should of course be unique (hint: it is not).

Debugging the weird behaviour

Suspecting this behaviour has something to do with the pointer, lets add a print statement to see what the pointer value is:


let p = Unmanaged.passUnretained(key).toOpaque()
print("Pointer: \(p)")

This resulted in the following output:


Pointer: 0x000000010120b590
OK
Pointer: 0x000000010104ab00
OK
Pointer: 0x000000010120b590
ERR 42
Program ended with exit code: 1

Finally, this the explains the behaviour we’re seeing; Different instances of DispatchSpecificKey share the same pointer!

Presumably ARC cleans up the key variable after we’re no longer using it and in a next iteration of the loop, the same memory location is reused again to store a new instance of DispatchSpecificKey.

Possible fixes for the bug

There are different solutions to fix this unexpected behaviour. One is to simply keep an array of each DispatchSpecificKey that gets created, that way no two DispatchSpecificKeys get assigned to the same memory location.

In my own code, I happened to have the key be a member of an object. The solution was to add a deinitializer to the object:


class MyObject {
  let key = DispatchSpecificKey<Int>()

  deinit {
    DispatchQueue.main.setSpecific(key: key, value: nil)
  }
}

A more general solution would be if DispatchSpecificKey were to be updated to clean up after itself:


public final class DispatchSpecificKey<T> {
  public init() {}

  // Is it OK to keep strong references to queues?
  internal var queues: [DispatchQueue] = []

  deinit {
    for queue in queues {
      queue.setSpecific(key: self, value: nil)
    }
  }
}

Closing thoughts

It took us a lot of debugging to finally narrow down the source of our bug to this reusing of the same memory address by two distinct objects. Normally in Swift code this wouldn’t be a problem, but because the underlying C API uses just pointers to compare for equality, this suddenly matters.

For the C code, it is arguable that the programmer should be responsible for cleaning up after they’re done with a C “dispatch specific key”. They should call dispatch_queue_set_specific with a nil value themselves.

But for the Swift code; The DispatchSpecificKey class really implies programmers shouldn’t have to know about the pointer internals of the C API. So in my opinion that class should have the cleanup code build-in.