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.

autoreleasepool with async await
 
 
Q