Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

autoreleasepool with async await

I ran into a problem, I have a recursive function in which Data type objects are temporarily created, because of this, the memory expands until the entire recursion ends. It would just be fixed using autoreleasepool, but it can't be used with async await, and I really don't want to rewrite the code for callbacks. Is there any option to use autoreleasepool with async await functions? (I Googled one option, that the Task already contains its own autoreleasepool, and if you do something like that, it should work, but it doesn't, the memory is still growing)

func autoreleasepool<Result>(_ perform: @escaping () async throws -> Result) async throws -> Result {
    try await Task {
        try await perform()
    }.value
}

(For those reading along at home, if you’re not sure what an autorelease pool is, see Objective-C Memory Management for Swift Programmers.)

Yeah, so this is tricky. What you’re asking for doesn’t exist, and I’m not sure it would actually help. The concurrency runtime should drain the autorelease pool when a job returns [1], so if this memory were being held by an autorelease pool then that should clear it up. And my tests suggest that this is indeed the case.

Consider this code:

class Canary {
    deinit { print("chirrr… argh!")}
}

func test() async throws {
    print("A")
    do {
        let c = Canary()
        _ = Unmanaged.passRetained(c).autorelease()
    }
    print("B")
    try await Task.sleep(for: .milliseconds(100))
    print("C")
}

try await test()

Running it from Xcode 16.2 on macOS 15.3.1, it prints:

A
B
chirrr… argh!
C

The autoreleases allowed the canary to escape the do { } block, but then it got cleaned up when the job ended. Setting a breakpoint on the deinitialiser reveals exactly the backtrace I’d expect:

(lldb) bt
* thread #2, stop reason = breakpoint 1.2
  * … #0: … xxst`Canary.__deallocating_deinit() at main.swift:0
    … #1: … libswiftCore.dylib`_swift_release_dealloc + 56
    … #2: … libswiftCore.dylib`bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 160
    … #3: … libobjc.A.dylib`objc_autoreleasePoolPop + 56
    … #4: … libswift_Concurrency.dylib`swift::runJobInEstablishedExecutorContext(swift::Job*) + 452
    … #5: … libswift_Concurrency.dylib`swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 144
    … #6: … libdispatch.dylib`_dispatch_root_queue_drain + 404
    … #7: … libdispatch.dylib`_dispatch_worker_thread2 + 188
    … #8: … libsystem_pthread.dylib`_pthread_wqthread + 228

Note frames 5 through 3 here.

Honestly, I suspect that autorelease isn’t the issue here, but rather than something else is going on.

Are you able to reproduce this in a small example? If so, please post it here so that I can take a proper look.

If not, reply anyway, and I’ll see if I can think of some other way to dig into this.

Share and Enjoy

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

[1] In Swift concurrency, a job refers to the fragment of synchronous code that execute between awaits.

Thank you for your reply and the example of autorelease!

I realized that I was mistaken when I said that the problem was the lack of autoreleasepool. The problem was recursion, because those data type objects that I called temporary weren't really temporary. And until the recursion completes, the objects I initialized will not be released.

I have created a similar sample code. When I use the doWorkAsync function, the memory increases to 9.34 GB, and after the recursion is completed, the memory is about 30 MB. But when I use "while loop" (doWorkInLoop function), the objects are released correctly, and the amount of memory is about 30 MB throughout the execution of the entire code.

In my code, I just rewrote the recursion into a loop, and it helped me.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            let worker = Worker()
            do {
                try await worker.doWorkAsync()
                print("Done")
            } catch {
                print("catch")
            }
        }
    }


}

class Worker {
    
    var counter: Int = 0
    
    func doWorkAsync() async throws {
        guard counter < 10000 else {
            return
        }
        var randomData = Data(count: 1024 * 1024)
        let result = randomData.withUnsafeMutableBytes {
            SecRandomCopyBytes(kSecRandomDefault, 1024 * 1024, $0.baseAddress!)
        }
        guard result == errSecSuccess else {
            throw NSError(domain: "error in random", code: 1)
        }
        try await Task.sleep(nanoseconds: 100_000)
        counter += 1
        try await doWorkAsync()
    }
    
    func doWorkInLoop() async throws {
        while true {
            guard counter < 10000 else {
                return
            }
            var randomData = Data(count: 1024 * 1024)
            let result = randomData.withUnsafeMutableBytes {
                SecRandomCopyBytes(kSecRandomDefault, 1024 * 1024, $0.baseAddress!)
            }
            guard result == errSecSuccess else {
                throw NSError(domain: "error in random", code: 1)
            }
            try await Task.sleep(nanoseconds: 100_000)
            counter += 1
        }
    }
}
autoreleasepool with async await
 
 
Q