Проблемы с циклом сохранения с использованием 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
}
}