Проблемы с циклом сохранения с использованием AsyncStream в задаче

Обнаружил эту проблему при работе с новыми инструментами параллелизма Swift.

Вот настройка:

      class FailedDeinit {
    
    init() {
        print(#function, id)
        task = Task {
            await subscribe()
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

var instance: FailedDeinit? = FailedDeinit()
instance = nil

Запуск этого кода на игровой площадке дает следующее:

init() F007863C-9187-4591-A4F4-BC6BC990A935

!!! deinitметод никогда не вызывается!!!

Странно, когда я меняю код на это:

      class SuccessDeinit {
    
    init() {
        print(#function, id)
        task = Task {
            let stream = AsyncStream<Double> { _ in }
            for await p in stream {
                print("\(p)")
            }
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

var instance: SuccessDeinit? = SuccessDeinit()
instance = nil

Переместив код из методаsubscribe()непосредственно в Задаче результат в консоли меняется на такой:

      init() 0C455201-89AE-4D7A-90F8-D6B2D93493B1
deinit 0C455201-89AE-4D7A-90F8-D6B2D93493B1

Это может быть ошибка или нет, но определенно есть что-то, чего я не понимаю. Я приветствовал бы любую информацию об этом.

~!~!~!~!

Это безумие (или, может быть, я?), Но с проектом SwiftUI macOS. Я все еще не получаю такое же поведение, как вы. Посмотрите на этот код, где я сохранил то же определениеFailedDeinitиSuccessDeinitклассы, но использовали их в представлении SwiftUI.

      struct ContentView: View {
    @State private var failed: FailedDeinit?
    @State private var success: SuccessDeinit?
    var body: some View {
        VStack {
            HStack {
                Button("Add failed") { failed = .init() }
                Button("Remove failed") { failed = nil }
            }
            HStack {
                Button("Add Success") { success = .init() }
                Button("Remove Success") { success = nil }
            }
        }
    }
}


class FailedDeinit {
    
    init() {
        print(#function, id)
        task = Task { [weak self] in
            await self?.subscribe()
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

2 ответа

Рассмотрим следующее:

      task = Task {
    await subscribe()
}

Это правда, что вводит сильную ссылку на . Вы можете разрешить эту сильную ссылку с помощью:

      task = Task { [weak self] in
    await self?.subscribe()
}

Но это только часть проблемы здесь. Этот[weak self]pattern помогает нам в этом случае только в том случае, если он еще не начался или завершился.

Проблема в том, что как только начнется выполнение, несмотря на ссылку в замыкании, будет сохраняться сильная ссылка до завершения. Итак, этоweakссылка разумна, но это не вся история.

Здесь проблема более тонкая, чем кажется на первый взгляд. Рассмотрим следующее:

      func subscribe() async {
    let stream = AsyncStream<Double> { _ in }
    for await p in stream {
        print("\(p)")
    }
}

Метод будет выполняться до тех пор, пока поток не вызовет . Но ты никогда неstream.(Вы неyieldлибо любые значения. Lol.) В любом случае, без чего-либо в , однажды начав, он никогда не завершится и, следовательно, никогда не выйдетself.

Итак, давайте рассмотрим вашу вторую версию, когда вы создаете , минуяsubscribe:

      task = Task {
    let stream = AsyncStream<Double> { _ in }
    for await p in stream {
        print("\(p)")
    }
}

Да, вы увидите, что объект будет освобожден, но вы также не заметите, что это никогда не закончится! Итак, не поддавайтесь ложному чувству безопасности только потому, что содержащий объект был освобожден: это никогда не заканчивается! Память, связанная с этим, никогда не будет освобождена (даже если родительский объект,FailedDeinitв вашем примере есть).

Все это можно проиллюстрировать, изменив ваш поток, чтобы фактически получить значения и, в конечном итоге:

      task = Task {
    let stream = AsyncStream<Double> { continuation in
        Task {
            for i in 0 ..< 10 {
                try await Task.sleep(nanoseconds: 1 * NSEC_PER_SECOND)
                continuation.yield(Double(i))
            }
            continuation.finish()
        }
    }

    for await p in stream {
        print("\(p)")
    }

    print("all done")
}

В этом случае, если вы закроете его во время выполнения потока, вы увидите, что он продолжается до тех пор, пока не завершится.(И если вам случится сделать это внутри метода, рассматриваемый объект также будет сохранен до тех пор, пока задача не будет отменена.)

Итак, что вам нужно сделать, этоTaskесли вы хотите, чтобы закончить. И вы также должны реализоватьonTerminationпродолжения таким образом, что это останавливает асинхронный поток.

Но результат таков, что если яcancelэто когда контроллер представления (или что-то еще) освобождается, тогда мой пример, дающий значения от 0 до 9, остановится, и задача будет освобождена.

Все сводится к тому, что вы действительно делаете. Но в процессе упрощения MCVE и удаления содержимогоAsyncStream, вы одновременно не обрабатываете отмену и никогда не звонитеfinish. Эти два вместе проявляют проблему, которую вы описываете.

На самом деле это не имеет ничего общего с async/await или AsyncStream. Это совершенно нормальный цикл удержания. Вы (экземпляр FailedDeinit) сохраняете задачу, но задача относится кsubscribeкоторый является методом вас, т.е.self, поэтому задача удерживает вас. Поэтому просто разорвите цикл удержания, как если бы вы разорвали любой другой цикл удержания. Просто измените

          task = Task {
        await subscribe()
    }

К

          task = Task { [weak self] in
        await self?.subscribe()
    }

Также обязательно тестируйте в реальном проекте, а не на игровой площадке, так как игровые площадки ни о чем не говорят в этом отношении. Вот код, который я использовал:

      import UIKit

class FailedDeinit {

    init() {
        print(#function, id)
        task = Task { [weak self] in
            await self?.subscribe()
        }
    }

    deinit {
        print(#function, id)
    }

    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }

    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        var instance: FailedDeinit? = FailedDeinit()
        instance = nil
   }
}
Другие вопросы по тегам