@@ -98,7 +98,13 @@ public class SessionDataTask: @unchecked Sendable {
9898 let onTaskDone = Delegate < ( Result < ( Data , URLResponse ? ) , KingfisherError > , [ TaskCallback ] ) , Void > ( )
9999 let onCallbackCancelled = Delegate < ( CancelToken , TaskCallback ) , Void > ( )
100100
101- var started = false
101+ private var _started = false
102+ var started : Bool {
103+ lock. lock ( )
104+ defer { lock. unlock ( ) }
105+ return _started
106+ }
107+
102108 var containsCallbacks : Bool {
103109 // We should be able to use `task.state != .running` to check it.
104110 // However, in some rare cases, cancelling the task does not change
@@ -154,8 +160,14 @@ public class SessionDataTask: @unchecked Sendable {
154160 }
155161
156162 func resume( ) {
157- guard !started else { return }
158- started = true
163+ // Atomic check-and-set; `task.resume()` is called outside the lock.
164+ lock. lock ( )
165+ guard !_started else {
166+ lock. unlock ( )
167+ return
168+ }
169+ _started = true
170+ lock. unlock ( )
159171 task. resume ( )
160172 }
161173
@@ -167,16 +179,9 @@ public class SessionDataTask: @unchecked Sendable {
167179 }
168180
169181 func forceCancel( ) {
170- // `callbacksStore` is protected by `lock` for every other access. Snapshot the
171- // tokens under the lock before cancelling, for two reasons:
172- // 1. `forceCancel()` is reachable from the public `ImageDownloader.cancelAll()` and
173- // `cancel(url:)` on arbitrary caller threads, while the session delegate queue may
174- // concurrently mutate the store via `addCallback`/`completeAndRemoveAllCallbacks`.
175- // Reading the live `keys` view here would be a data race on the dictionary.
176- // 2. `cancel(token:)` removes the token via `removeCallback`, so iterating the live
177- // `keys` view would mutate the dictionary mid-iteration.
178- // Iterating an immutable snapshot avoids both. The lock is released before calling
179- // `cancel(token:)`, which re-acquires it (the non-recursive `lock` would otherwise deadlock).
182+ // Snapshot the tokens under the lock, then cancel outside of it: `forceCancel` can run on
183+ // any thread while `callbacksStore` is being mutated, and `cancel(token:)` re-acquires the
184+ // non-recurrent lock.
180185 lock. lock ( )
181186 let tokens = Array ( callbacksStore. keys)
182187 lock. unlock ( )
0 commit comments