29

Okay so we all know that in traditional concurrency in Swift, if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self] in, like this:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}

This is to stop us strongly capturing self in the closure and causing unnecessary retention/inadvertently referencing other entities that have dropped out of memory already.

How about in async/await? I'm seeing conflicting things online so I'm just going to post two examples to the community and see what you think about both:

class AsyncClass {
   func function1() async {
      let result = await performNetworkRequestAsync()
      self.printSomething()
   }

   func function2() {
      Task { [weak self] in
         let result = await performNetworkRequestAsync()
         self?.printSomething()         
      }
   }

   func function3() {
      apiClient.performRequest { [weak self] result in
         self?.printSomething()
      }
   }

   func printSomething() {
      print("Something")
   }
}

function3 is straightforward - old fashioned concurrency means using [weak self]. function2 I think is right, because we're still capturing things in a closure so we should use [weak self]. function1 is this just handled by Swift, or should I be doing something special here?

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Matt Beaney
  • 460
  • 4
  • 15

2 Answers2

54

Bottom line, there is often little point in using [weak self] capture lists with Task objects. Use cancelation patterns instead.


A few detailed considerations:

  1. Weak capture lists are not required.

    You said:

    in traditional concurrency in Swift, if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self]

    This is not true. Yes, it may be prudent or advisable to use the [weak self] capture list, but it is not required. The only time you “must” use a weak reference to self is when there is a persistent strong reference cycle.

    For well-written asynchronous patterns (where the called routine releases the closure as soon as it is done with it), there is no persistent strong reference cycle risk. The [weak self] is not required.

  2. Nonetheless, weak capture lists are useful.

    Using [weak self] in these traditional escaping closure patterns still has utility. Specifically, in the absence of the weak reference to self, the closure will keep a strong reference to self until the asynchronous process finishes.

    A common example is when you initiate a network request to show some information in a scene. If you dismiss the scene while some asynchronous network request is in progress, there is no point in keeping the view controller in memory, waiting for a network request that merely updates the associated views that are long gone.

    Needless to say, the weak reference to self is really only part of the solution. If there’s no point in retaining self to wait for the result of the asynchronous call, there is often no point in having the asynchronous call continue, either. E.g., we might marry a weak reference to self with a deinit that cancels the pending asynchronous process.

  3. Weak capture lists are less useful in Swift concurrency.

    Consider this permutation of your function2:

    func function2() {
        Task { [weak self] in
            let result = await apiClient.performNetworkRequestAsync()
            self?.printSomething()         
        }
    }
    

    This looks like it should not keep a strong reference to self while performNetworkRequestAsync is in progress. But the reference to a property, apiClient, will introduce a strong reference, without any warning or error message. E.g., below, I let AsyncClass fall out of scope at the red signpost, but despite the [weak self] capture list, it was not released until the asynchronous process finished:

    enter image description here

    The [weak self] capture list accomplishes very little in this case. Remember that in Swift concurrency there is a lot going on behind the scenes (e.g., code after the “suspension point” is a “continuation”, etc.). It is not the same as a simple GCD dispatch. See Swift concurrency: Behind the scenes.

    If, however, you make all property references weak, too, then it will work as expected:

    func function2() {
        Task { [weak self] in
            let result = await self?.apiClient.performNetworkRequestAsync()
            self?.printSomething()         
        }
    }
    

    Hopefully, future compiler versions will warn us of this hidden strong reference to self.

  4. Make tasks cancelable.

    Rather than worrying about whether you should use weak reference to self, one could consider simply supporting cancelation:

    var task: Task<Void, Never>?
    
    func function2() {
        task = Task {
            let result = await apiClient.performNetworkRequestAsync()
            printSomething()
            task = nil
        }
    }
    

    And then,

    @IBAction func didTapDismiss(_ sender: Any) {
        task?.cancel()
        dismiss(animated: true)
    }
    

    Now, obviously, that assumes that your task supports cancelation. Most of the Apple async API does. (But if you have written your own withUnsafeContinuation-style implementation, then you will want to periodically check Task.isCancelled or wrap your call in a withTaskCancellationHandler or other similar mechanism to add cancelation support. But this is beyond the scope of this question.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 4
    "Hopefully, future compiler versions will warn us of this hidden strong reference to self." It seems that the compiler in Xcode 13.4 treats this as a warning, but it will become an error in Swift 6. – Greg Brown Jun 02 '22 at 15:42
13

if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self] in, like this

This isn't quite true. When you create a closure in Swift, the variables that the closure references, or "closes over", are retained by default, to ensure that those objects are valid to use when the closure is called. This includes self, when self is referenced inside of the closure.

The typical retain cycle that you want to avoid requires two things:

  1. The closure retains self, and
  2. self retains the closure back

The retain cycle happens if self holds on to the closure strongly, and the closure holds on to self strongly — by default ARC rules with no further intervention, neither object can be released (because something has retained it), so the memory will never be freed.

There are two ways to break this cycle:

  1. Explicitly break a link between the closure and self when you're done calling the closure, e.g. if self.action is a closure which references self, assign nil to self.action once it's called, e.g.

    self.action = { /* Strongly retaining `self`! */
        self.doSomething()
    
        // Explicitly break up the cycle.
        self.action = nil
    }
    

    This isn't usually applicable because it makes self.action one-shot, and you also have a retain cycle until you call self.action(). Alternatively,

  2. Have either object not retain the other. Typically, this is done by deciding which object is the owner of the other in a parent-child relationship, and typically, self ends up retaining the closure strongly, while the closure references self weakly via weak self, to avoid retaining it

These rules are true regardless of what self is, and what the closure does: whether network calls, animation callbacks, etc.

With your original code, you only actually have a retain cycle if apiClient is a member of self, and holds on to the closure for the duration of the network request:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}

If the closure is actually dispatched elsewhere (e.g., apiClient does not retain the closure directly), then you don't actually need [weak self], because there was never a cycle to begin with!

The rules are exactly the same with Swift concurrency and Task:

  1. The closure you pass into a Task to initialize it with retains the objects it references by default (unless you use [weak ...])
  2. Task holds on to the closure for the duration of the task (i.e., while it's executing)
  3. You will have a retain cycle if self holds on to the Task for the duration of the execution

In the case of function2(), the Task is spun up and dispatched asynchronously, but self does not hold on to the resulting Task object, which means that there's no need for [weak self]. If instead, function2() stored the created Task, then you would have a potential retain cycle which you'd need to break up:

class AsyncClass {
    var runningTask: Task?

    func function4() {
        // We retain `runningTask` by default.
        runningTask = Task {
            // Oops, the closure retains `self`!
            self.printSomething()
        }
    }
}

If you need to hold on to the task (e.g. so you can cancel it), you'll want to avoid having the task retain self back (Task { [weak self] ... }).

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Great answer. John Sundell also has a *lot* of posts on the new swift concurrency which go into depth and provide many examples, including many of the edge cases. – flanker Apr 03 '22 at 19:41
  • So imagine `apiClient` contains a reference to a `URLSession`, and the `URLSession` performs the network request, and the completion you see in my example is called from the completion of that network request (e.g. `dataTaskWithUrl` or something). Presumably the `URLSession` retains the closure for the duration of the request? – Matt Beaney Apr 04 '22 at 09:37
  • 1
    @MattBeaney If `apiClient` has a strong reference to the `URLSession` you’re using, then yes, you’ll have a retain cycle for the duration of the request. However, as soon as the request is done, `URLSession` should dispose of the closure (there’s no reason to hold on to it forever), so the cycle should break. It’s marginally safer to use `[weak self]` in that case, but shouldn’t be _necessary_. (This is like the `action` example in my answer.) – Itai Ferber Apr 04 '22 at 11:15
  • 1
    @Rob Ah, good call! Will update. – Itai Ferber Apr 04 '22 at 16:08
  • 1
    @Rob Thanks for the feedback again — took it out to avoid confusing thins unnecessarily. – Itai Ferber Apr 04 '22 at 16:49
  • Reference cycles are certainly the most obvious (and important) reason to use weak self-references, but I'd argue that network requests are a close second. If the user dismisses a controller while a request is outstanding, a strong reference will prevent the controller from being released. The response handler may then attempt to perform an action on the controller's view, which is no longer in the hierarchy. – Greg Brown Jun 02 '22 at 15:25