NWConnection and DispatchQueue Lifecycle During Connection Teardown

I’m using Apple’s Network framework to implement a UDP client using NWConnection, and I have a question regarding the lifecycle of the DispatchQueue associated with an NWConnection instance.

Let's assume I have an NWConnection instance, and I associate it with a dispatch queue using the start(queue:) API, such that network OS events for the NWConnection instance can be delivered to this queue.

My understanding is that this association would result in NWConnection holding a strong reference to the DispatchQueue object.

Now, I perform some I/O (send/receive) on the NWConnection instance and immediately perform the following steps. Also, assume that the completion closures for those I/O operations do not capture or otherwise retain the NWConnection.

  1. Call connection.cancel() and then release my last strong reference to the NWConnection.
  2. Without waiting for the connection to transition to the .cancelled state, I also release my last strong reference to the associated DispatchQueue.

My question is:

Does NWConnection, during its teardown, retain the DispatchQueue until the cancellation completions for all pending I/O operations associated with the connection have been delivered/executed, given that the application no longer holds any strong references to either the NWConnection or the DispatchQueue?

Or, once cancel() is called, does NWConnection immediately release its reference to the DispatchQueue, in which case whether the pending callbacks are ultimately executed depends on whether the application has kept the queue alive?

Answered by DTS Engineer in 898956022
this … would result in NWConnection holding a strong reference to the DispatchQueue object.

Correct.

whether the pending callbacks are ultimately executed depends on whether the application has kept the queue alive?

That’s never the case. The connection will maintain the strong reference until it’s completely done with the queue.

You can break this up into two cases:

  • Completion handlers
  • Event handlers

For completion handlers the connection is required to call all completion handlers once and only once. It’s not safe to simple drop them. And the completion handlers must run on the connection’s queue. Thus, the connection must retain its strong reference to the queue until all completion handlers have returned.

For event handlers — state update handlers and so on — my advice is that you set the handler property to nil before calling cancel(). This isn’t because of the issue you’re concerned with, but rather because earlier versions of Network framework would generate retain loops if you didn’t do that [1] )-:

Here’s how I usually handle this:

  1. If I’m not already running on the connection’s queue, I bounce to that.
  2. I update my internal state to indicate that the connection is done.
  3. I set all event handlers to nil.
  4. I cancel the connection.

If any events were pending during this sequence, or were generated by this sequence, then I’m safe because:

  • If the event happens after setting the handler to nil, it won’t be delivered.
  • If the event happened before that, it can’t be delivered in the middle of the sequence because I’m running on the connection’s queue. Once my code returns and the queue is freed up, the event handler can run. I then have it check the internal state, as set by step 2, and immediately return if the connection is done.

Having said that, I see a lot of code that takes an opposite tack, and triggers all of its clean up on the .cancel event. I’m not massively opposed to that approach. I originally adopted my approach without a good reason, and only years later did I learn that it was protecting me from the above-mentioned retain loop!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] See macOS Ventura 13 Release Notes.

this … would result in NWConnection holding a strong reference to the DispatchQueue object.

Correct.

whether the pending callbacks are ultimately executed depends on whether the application has kept the queue alive?

That’s never the case. The connection will maintain the strong reference until it’s completely done with the queue.

You can break this up into two cases:

  • Completion handlers
  • Event handlers

For completion handlers the connection is required to call all completion handlers once and only once. It’s not safe to simple drop them. And the completion handlers must run on the connection’s queue. Thus, the connection must retain its strong reference to the queue until all completion handlers have returned.

For event handlers — state update handlers and so on — my advice is that you set the handler property to nil before calling cancel(). This isn’t because of the issue you’re concerned with, but rather because earlier versions of Network framework would generate retain loops if you didn’t do that [1] )-:

Here’s how I usually handle this:

  1. If I’m not already running on the connection’s queue, I bounce to that.
  2. I update my internal state to indicate that the connection is done.
  3. I set all event handlers to nil.
  4. I cancel the connection.

If any events were pending during this sequence, or were generated by this sequence, then I’m safe because:

  • If the event happens after setting the handler to nil, it won’t be delivered.
  • If the event happened before that, it can’t be delivered in the middle of the sequence because I’m running on the connection’s queue. Once my code returns and the queue is freed up, the event handler can run. I then have it check the internal state, as set by step 2, and immediately return if the connection is done.

Having said that, I see a lot of code that takes an opposite tack, and triggers all of its clean up on the .cancel event. I’m not massively opposed to that approach. I originally adopted my approach without a good reason, and only years later did I learn that it was protecting me from the above-mentioned retain loop!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] See macOS Ventura 13 Release Notes.

NWConnection and DispatchQueue Lifecycle During Connection Teardown
 
 
Q