Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Race Condition in ImageDownloader.addDownloadTask #2231

Open
hotngui opened this issue Apr 15, 2024 · 0 comments
Open

Race Condition in ImageDownloader.addDownloadTask #2231

hotngui opened this issue Apr 15, 2024 · 0 comments

Comments

@hotngui
Copy link

hotngui commented Apr 15, 2024

Check List

Thanks for considering to open an issue. Before you submit your issue, please confirm these boxes are checked.

Issue Description

Calling KingfisherManager.shared.retrieveImage(with:) multiple times on a concurrent queue results in a race condition in the addDownloadTask(context:,callback:) method.

What

It causes the if/then/else to fail much of the time leaving some attempts to add new tasks to fall on the floor. The underlying sessionDelegate.task(for:) does have a lock, but that does not help the expression being tested by the if statement.

Reproduce

The following SwiftUI view can be used to reproduce the problem. We've seen it fail to update completionCount to the correct value most of the time. Yes, this example code is verify contrived but it was derived from a much more complicated piece of code that we cannot share.

struct ContentView: View {
    let retrievalAttemptCount = 5
    @State var completionCount = 0

    var body: some View {
        VStack(spacing: 16) {
            Button("Clear cache & reset") {
                KingfisherManager.shared.cache.clearCache()
                completionCount = 0
            }
            Button("Download images") {
                completionCount = 0

                guard let imageURL = URL(string: "https://images.pexels.com/photos/96938/pexels-photo-96938.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2")
                else {
                    return
                }

                for _ in 1...retrievalAttemptCount {
                    DispatchQueue.global(qos: .default).async {
                        KingfisherManager.shared.retrieveImage(with: imageURL) { result in
                            Task { @MainActor in
                                completionCount += 1
                            }
                        }
                    }
                }
            }

            Text("Completion count: \(completionCount)/\(retrievalAttemptCount)")

            Spacer()
        }
        .padding()
    }
}

Other Comment

I played around with a naive solution by adding the following code to the top of the addDownloadTask(context:,callback:) method and it seems to avoid the race condition.

        lock.lock()
        defer { lock.unlock() }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant