Try to move the SDWebImageDownloaderOperation dataTask property initialization into synchronized lock to avoid thread-safe issue#3856
Conversation
WalkthroughThis change modifies Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10–15 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…alization into synchronized lock to avoid thread-safe issue
29a3921 to
ca209c3
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m (1)
235-255: Consider recheckingisCancelledbefore assigningself.dataTask/executingand resumingThere is still a small window between the first and second
@synchronized(self)blocks wherecancelcan run:
starthas already createddataTaskand left the first@synchronized(self).- Another thread calls
cancel, which (viacancelInternalandreset) can:
- Fire completion blocks and clear
callbackTokens.- Invalidate the session.
- Leave the operation in a cancelled state (
isCancelled == YES) but withisExecuting == NOandisFinished == NO.After that, the second
@synchronized(self)instartwill still execute, setself.dataTask = dataTask; self.executing = YES;, and[self.dataTask resume];, effectively “reviving” an operation that was just cancelled and had its callbacks fired.This is a pre‑existing race, not introduced by this PR, but it becomes more visible now that
dataTaskhandling is tightened. To align cancel semantics with expectations (cancel-before-or-during-start should prevent the request from ever starting), you could recheck cancellation inside the second synchronized block before assigning state:- if (dataTask) { + if (dataTask) { if (self.options & SDWebImageDownloaderHighPriority) { dataTask.priority = NSURLSessionTaskPriorityHigh; } else if (self.options & SDWebImageDownloaderLowPriority) { dataTask.priority = NSURLSessionTaskPriorityLow; } else { dataTask.priority = NSURLSessionTaskPriorityDefault; } - NSArray<SDWebImageDownloaderOperationToken *> *tokens; - @synchronized (self) { - tokens = [self.callbackTokens copy]; - self.dataTask = dataTask; - self.executing = YES; - } + BOOL shouldStart = NO; + NSArray<SDWebImageDownloaderOperationToken *> *tokens = nil; + @synchronized (self) { + if (!self.isCancelled && !self.isFinished) { + tokens = [self.callbackTokens copy]; + self.dataTask = dataTask; + self.executing = YES; + shouldStart = YES; + } + } + if (!shouldStart) { + // Operation was cancelled/finished after task creation; do not resume. + return; + } for (SDWebImageDownloaderOperationToken *token in tokens) { ... } // Put the URLSession task resume at the end of start method to avoid race condition [self.dataTask resume]; ...This keeps the “resume at end” behavior, but guarantees you never mark the operation executing or resume a task after a concurrent cancel/reset has already run.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Unit Test (visionOS)
- GitHub Check: Unit Test (macOS)
- GitHub Check: Unit Test (iOS)
- GitHub Check: Cocoapods Demo
- GitHub Check: Cocoapods Lint
🔇 Additional comments (1)
SDWebImage/Core/SDWebImageDownloaderOperation.m (1)
170-233:dataTaskcreation inside@synchronized(self)is a solid thread-safety improvementCreating the
NSURLSessionTaskinside the synchronized block and using a localdataTaskvariable avoids unsynchronized writes toself.dataTaskversuscancel/reset, and keeps the lock held only while touching shared state. This change looks correct and clearly improves the data‑task lifetime handling aroundstart.
New Pull Request Checklist
I have read and understood the CONTRIBUTING guide
I have read the Documentation
I have searched for a similar pull request in the project and found none
I have updated this branch with the latest master to avoid conflicts (via merge from master or rebase)
I have added the required tests to prove the fix/feature I am adding
I have updated the documentation (if necessary)
I have run the tests and they pass
I have run the lint and it passes (
pod lib lint)This merge request fixes / refers to the following issues: ...
Pull Request Description
This close #3842
Maybe there are better way to fix, but this is the simplest one in my mind
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.