Memory leak issue while asynchronously iterating over async sequence

I am having memory leak issue while iterating over async sequence like so:

import Foundation
import Combine

class MyServiceClass {
    
    let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect().values
    
    init() {
        Task { [weak self] in
            await self?.setUpStreamIteration()
        }
    }
    
    func setUpStreamIteration() async {
        for await time in timerSequence {
            print(time)
        }
    }
}

var service: MyServiceClass? = MyServiceClass()
service = nil

the task continues to run and print the time even after the screen\class is "dismissed".
Output:

2023-03-26 00:14:11 +0000
2023-03-26 00:14:12 +0000
2023-03-26 00:14:13 +0000
2023-03-26 00:14:14 +0000
2023-03-26 00:14:15 +0000
2023-03-26 00:14:16 +0000
2023-03-26 00:14:17 +0000
2023-03-26 00:14:18 +0000
2023-03-26 00:14:19 +0000
...

but when using .sink mechanism instead - I don't experience and memory leak.
the working example is:

import Foundation
import Combine

class MyServiceClass {
    
    let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect()

    
    init() {
        Task { [weak self] in
            await self?.setUpStreamIteration()
        }
    }
    
    func setUpStreamIteration() async {
         await timerSequence
                        .sink { [weak self] time in
                            print(time)
                        }
                        .store(in: &cancellations)
    }
}

var service: MyServiceClass? = MyServiceClass()
service = nil

can any one try to explain why using .sink managed to prevent the memory leak?
is it ok to use .sink always as oppose of iterating over async sequence?
can we compare the two approaches - to decide which one is better and why?
thanks!

Since setUpStreamIteration will never finish, you just keep a strong ref of self.

You can check the following post for more detail

#02 – Not understanding that Task automatically captures self

Suggested code :point_down:

class MyServiceClass {
    
    let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect().values
    
    init() {
        Task { [weak self] in
            guard let timerSequence = self?.timerSequence else { return }
            for await time in timerSequence {
                guard let self else { return }
                print(time)
            }
        }
    }
}
2 Likes

thanks @Kyle-Ye , but in the post you referred me to has no explanation on using .sink over setUpStreamIteration. why wouldn't I be using .sink always?

sink is an API of Combine framework (Only available on Apple platforms)
AsyncSequence and Task are Swift standard library API (Available on all platform Swift supports)

1 Like

The code you suggested unwraps await self?.setUpStreamIteration() to avoid self reference. But what if the function setUpStreamIteration is quite long and complex to be unwrapped into a single Task closure? Would there be any better, scalable solution?

I had same issue while ago but still struggling to find better solution without manually canceling that task because of that I completely avoiding AsyncSequence OR stream !