What is the purpose of `setTarget(queue:)` if it always crashes?

I'm currently building a graph like framework where each node has a dispatch queue. To avoid possible context switching I'd like to set the target of a descendant node to the furthest direct ancestor node with similar dispatch queue context. The graph itself is tree-alike and avoids strong reference cycles by design. However I cannot proceed with that optimization because the setTarget(queue:) method always crashes for custom queues, even in the following simple example:

import Dispatch

DispatchQueue.main.setTarget(queue: DispatchQueue.main) // Okay
DispatchQueue.global().setTarget(queue: DispatchQueue.global()) // Okay
DispatchQueue.global(qos: .utility).setTarget(queue: DispatchQueue.global(qos: .background)) // Okay

let a = DispatchQueue(label: "a", attributes: .concurrent)
a.async {}
let b = DispatchQueue(label: "b", target: a) // Okay
b.async {}
let c = DispatchQueue(label: "c")
c.setTarget(queue: a) // Crashes
// or
c.setTarget(queue: c) // Also crashes (but it worked above with the main queue)

/*
 error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
 The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

 * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
 * frame #0: 0x0000000108b72730 libdispatch.dylib`_dispatch_lane_set_target_queue + 404
 frame #1: 0x00007f8660304800
 frame #2: 0x000000011e731742 $__lldb_expr20`main at Untitled Page 2.xcplaygroundpage:8
 frame #3: 0x000000010594f600 Test`linkResources + 304
 frame #4: 0x00000001071c5a3c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
 frame #5: 0x00000001071c51f0 CoreFoundation`__CFRunLoopDoBlocks + 336
 frame #6: 0x00000001071bfa64 CoreFoundation`__CFRunLoopRun + 1284
 frame #7: 0x00000001071bf221 CoreFoundation`CFRunLoopRunSpecific + 625
 frame #8: 0x000000010f07f1dd GraphicsServices`GSEventRunModal + 62
 frame #9: 0x000000010aeb6115 UIKitCore`UIApplicationMain + 140
 frame #10: 0x000000010594f6cd Test`main + 205
 frame #11: 0x0000000108bdc551 libdyld.dylib`start + 1
 frame #12: 0x0000000108bdc551 libdyld.dylib`start + 1
*/

Is this a bug or intended behavior? Can someone please explain if it should be possible to set the target at runtime and also potentially re-set or remove the target queue entirely?

Hello. Maybe it will help - from queue.h:

  • In general, changing the target queue of an object is an asynchronous
  • operation that doesn't take effect immediately, and doesn't affect blocks
  • already associated with the specified object.
  • However, if an object is inactive at the time dispatch_set_target_queue() is
  • called, then the target queue change takes effect immediately, and will
  • affect blocks already associated with the specified object. After an
  • initially inactive object has been activated, calling
  • dispatch_set_target_queue() results in an assertion and the process being
  • terminated.
  • If a dispatch queue is active and targeted by other dispatch objects,
  • changing its target queue results in undefined behavior.

It kind of makes a little sense, but why does it work with global queues?!

One more interesting comment for dispatch_queue_attr_make_with_autorelease_frequency:

  • The global concurrent queues have the DISPATCH_AUTORELEASE_FREQUENCY_NEVER
  • behavior. Manually created dispatch queues use
  • DISPATCH_AUTORELEASE_FREQUENCY_INHERIT by default.
  • Queues created with this attribute cannot change target queues after having
  • been activated. See dispatch_set_target_queue() and dispatch_activate().
let a = DispatchQueue(label: "a", attributes: .concurrent)
a.async {}
let b = DispatchQueue(label: "b", autoreleaseFrequency: .never)
b.setTarget(queue: a)

Still crashes :confused: That part is still confusing to me. I might need to create a pool of queues where the custom queues will always reference to during the init call to work around the problem.

I agree, it is still unclear. May be you can try to create queue inactive via

dispatch_queue_attr_make_initially_inactive

For anyone else seeing this, just wanted to mention that the final recommendation, i.e. mark as initiallyInactive, works, but that probably the best way to set a target is in the init method itself:

let queue = DispatchQueue(label: "com.me.queue", target: .global(qos: .default))

If you don't know the target at creation time, you can set the queue to be initially inactive, then set that target later (and don't forget to activate!)

let queue = DispatchQueue(label: "com.me.queue", attributes: .initiallyInactive)

// Some time later
queue.setTarget(queue: .main)
queue.activate()
1 Like