Swift Concurrencyの Task でのself参照
Swift ConcurrencyのTask#initのoperationクロージャー内の self
参照や [weak self]
は 普通の @escaping
なクロージャーとは違った部分があったため、PlayGroundでの挙動確認をメモする。
Closureで循環参照になるパターン
まず、[weak self]
を利用するよくあるパターンの @escaping
なクロージャーでの循環参照が発生する例について、例えば下記の様なコードで循環参照が発生する。
class Counter {
var count = 0
var closure: (() -> Void)?
deinit {
print("deinit")
}
func incrementOnClosure() {
self.closure = {
self.increment()
}
self.closure!()
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
var counter: Counter? = Counter()
counter?.incrementOnClosure()
counter = nil
// 出力結果
// count: 1
// -> deinitがコールされない
上記では下記の2つの参照が循環しているため、循環参照する。
- counter -> counter#closure
- counter#closure -> self(counter)
- closure内でselfをコールすると暗黙的にselfが強参照される。
この循環参照の解決策の1つとして [weak self]
を用いるよくあるパターンが下記。
class Counter {
var count = 0
var closure: (() -> Void)?
deinit {
print("deinit")
}
func incrementOnClosure() {
// [weak self] 追加
self.closure = { [weak self] in
self?.increment()
}
self.closure!()
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
var counter: Counter? = Counter()
counter?.incrementOnClosure()
counter = nil
// 出力結果
// count: 1
// deinit
こうすると、修正前の counter#closure -> self(counter)
の方向の強参照がなくなるため、counter = nil
のタイミングでcounterが解放される(deinitがコールされる)。
ちなみに、@escaping
なクロージャー内では [weak self]
(or [onowned self]
)しないと必ず循環参照になるというわけではなく、@escaping
なクロージャーを保持していてかつクロージャー内で self
参照すると循環参照した状態になるので、たとえば下記の様なクロージャーを保持していない場合では循環参照にならない。
class Counter {
var count = 0
deinit {
print("deinit")
}
func incrementOnClosure() {
let closure = {
self.increment()
}
closure()
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
var counter: Counter? = Counter()
counter?.incrementOnClosure()
counter = nil
// 出力結果
// count: 1
// deinit
Task内でのself参照
結論から、自分が試したところでは Taskのoperationクロージャーでは上記の様な循環参照は発生しなかった。だが、operationクロージャー内でselfを参照していると、Task実行中は強参照されるため、特にTask内で awaitする様な処理を行なっていた場合に注意が必要だと思った。
いろいろとコード例と実行結果を記載して行く。
普通にTask内でself参照
class Counter {
var count = 0
deinit {
print("deinit")
}
func incrementOnTask() {
Task {
self.increment()
}
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
var counter: Counter? = Counter()
counter?.incrementOnTask()
counter = nil
// 出力結果
// count: 1
// deinit
ちなみに、Taskのoperationクロージャー
は@_implicitSelfCaptureという属性が付いてて暗黙的にselfを利用できるため、selfを省略できる。
func incrementOnTask() {
Task {
increment() // self. を省略可能
}
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
Task内でawaitがあるときのself参照
class Counter {
var count = 0
deinit {
print("deinit")
}
func incrementOnAfter3Seconds() {
Task {
do {
try await Task.sleep(nanoseconds: 3_000_000_000)
self.increment()
} catch {
print(error)
}
}
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
Task {
var counter: Counter? = Counter()
counter?.incrementOnAfter3Seconds()
try! await Task.sleep(nanoseconds: 1_000_000_000)
counter = nil
}
// 出力結果(3秒後に下記が出力される)
// count: 1
// deinit
コード的には、counter?.incrementOnAfter3Seconds()
したあと即座に counter = nil
しているが、 Task
で self
をキャプチャしているため、Taskが終わるまで deinitはコールされない。
なのでこの挙動(counter = nil
しているのに selfが維持されてTask内のself.increment()
が実行される)を防止するための解決策として、 [weak self]
が利用できる。
上記のコードのTask部分を下記の様に[weak self]
を追記するとこうなる。
func incrementOnAfter3Seconds() {
Task { [weak self] in
do {
try await Task.sleep(nanoseconds: 3_000_000_000)
self?.increment()
} catch {
print(error)
}
}
}
// 出力結果
// deinit
// -> count: 1 は出力されない。
上記の [weak self]
のあとに guard let self = self else { return }
をすると、deinit
される前に self.increment()
が実行されるので注意が必要です。
func incrementOnAfter3Seconds() {
Task { [weak self] in
guard let self = self else { return }
do {
try await Task.sleep(nanoseconds: 3_000_000_000)
self?.increment()
} catch {
print(error)
}
}
}
// 出力結果
// count: 1
// deinit
// -> selfがTask内で強参照されておりはcount: 1を出力してからdeinitする。
[weak self]
書かずにTaskの処理を止める場合は task.cancel()
でもできる。
class Counter {
var count = 0
var task: Task<Void, Never>?
deinit {
print("deinit")
}
func incrementOnAfter3Seconds() {
task = Task {
do {
try await Task.sleep(nanoseconds: 3_000_000_000)
self.increment()
} catch {
print(error)
}
}
}
private func increment() {
self.count += 1
print("count: \(self.count)")
}
}
Task {
var counter: Counter? = Counter()
counter?.incrementOnAfter3Seconds()
try! await Task.sleep(nanoseconds: 1_000_000_000)
counter?.task?.cancel()
counter = nil
}
// 出力結果
// CancellationError()
// deinit
// -> `await Task.sleep()`中に `Task#cancel()` するとCancellationError()が発生してtaskが終了する。
参考
Discussion