I receive data items from my API, on a specific serial queue, that I use to calculate the view models to display in a collection view. The simplified code is:
var viewModels: [ViewModel] = [] // read and written on main queue
var _viewModels: [ViewModel] = [] // written in background, read in main queue
func didReceive(items: [Item]) {
// called on the same background serial queue
_viewModels = items.map { ViewModel($0) }
DispatchQueue.main.async {
self.viewModels = self._viewModels
self.updateCollectionView {
// UI update has finished
}
}
}
didReceive can be called at any time.
_viewModels is written always on the same background queue. After that I switch to main queue to cache the computed view models and use it in data source for displaying.
Can self.viewModels = self._viewModels lead to crashes? Do I have to use some kind of locking mechanism? If yes, how does it work when main thread is involved?
Crashes not necessarily, but this can lead to inconsistency. As _viewModels is declared outside the scope of didReceive, it is a shared resource that can be modified each time you enter didReceive. After dispatching this to the main thread, some other thread can enter in didReceive and change your value. So when you assign it to self.viewModels, you would get the "second" value.
I believe that declaring _viewModels inside didReceive would fix your issue, as you would have a reference for each run of didReceive:
var viewModels: [ViewModel] = [] // read and written on main queue
func didReceive(items: [Item]) {
// called on the same background serial queue
let _viewModels = items.map { ViewModel($0) }
DispatchQueue.main.async {
self.viewModels = _viewModels
self.updateCollectionView {
// UI update has finished
}
}
}
If you are not allowed to do this, try taking a look at Actors:
https://www.avanderlee.com/swift/actors/
Related
let serialQueue = DispatchQueue(label: "Serial Queue")
func performCriticalSectionTask() {
serialQueue.async {
performLongRuningAsyncTask()
}
}
func performLongRuningAsyncTask() {
/// some long running task
}
The function performCriticalSectionTask() can be called from different places many times.
I want this function to be running one at a time. Thus, I kept the critical section of code inside the serial async queue.
But, the problem here is that the critical section itself is a performLongRuningAsyncTask() which will return immediately, and thus serial queue will not wait for the current task to complete first and will start another one.
How can I solve this problem?
if performLongRuningAsyncTask is only running in one thread, it will be called only once at the time. In your case it delegates it to another thread, so you wrapping it into another thread call doesn't work since it will be on another thread anyway
You could do checks in the method itself, the simplest way is to add a boolean. (Or you could add these checks in your class that executes this method, with a completion handler).
Another ways are adding dispatch groups / semaphores / locks.
If you still need it to be executed later, you should use a dispatch group / OperationQueue / Semaphore.
func performLongRunningAsyncTask() {
self.serialQueue.sync {
if isAlreadyRunning {
return
}
isAlreadyRunning = true
}
asyncTask { result in
self.serialQueue.sync {
self.isAlreadyRunning = false
}
}
}
Background
I'm implementing a search. Each search query results in one DispatchWorkItem which is then queued for execution. As the user can trigger a new search faster than the previous one can be completed, I'd like to cancel the previous one as soon as I receive a new one.
This is my current setup:
var currentSearchJob: DispatchWorkItem?
let searchJobQueue = DispatchQueue(label: QUEUE_KEY)
func updateSearchResults(for searchController: UISearchController) {
let queryString = searchController.searchBar.text?.lowercased() ?? ""
// if there is already an (older) search job running, cancel it
currentSearchJob?.cancel()
// create a new search job
currentSearchJob = DispatchWorkItem() {
self.filter(queryString: queryString)
}
// start the new job
searchJobQueue.async(execute: currentSearchJob!)
}
Problem
I understand that dispatchWorkItem.cancel() doesn't kill the running task immediately. Instead, I need to check for dispatchWorkItem.isCancelled manually. But how do I get the right dispatchWorkItemobject in this case?
If I were setting currentSearchJob only once, I could simply access that attribute like done in this case. However, this isn't applicable here, because the attribute will be overriden before the filter() method will be finished. How do I know which instance is actually running the code in which I want to check for dispatchWorkItem.isCancelled?
Ideally, I'd like to provide the newly-created DispatchWorkItem as an additional parameter to the filter() method. But that's not possible, because I'll get a Variable used within its own initial value error.
I'm new to Swift, so I hope I'm just missing something. Any help is appreciated very much!
The trick is how to have a dispatched task check if it has been canceled. I'd actually suggest consider OperationQueue approach, rather than using dispatch queues directly.
There are at least two approaches:
Most elegant, IMHO, is to just subclass Operation, passing whatever you want to it in the init method, and performing the work in the main method:
class SearchOperation: Operation {
private var queryString: String
init(queryString: String) {
self.queryString = queryString
super.init()
}
override func main() {
// do something synchronous, periodically checking `isCancelled`
// e.g., for illustrative purposes
print("starting \(queryString)")
for i in 0 ... 10 {
if isCancelled { print("canceled \(queryString)"); return }
print(" \(queryString): \(i)")
heavyWork()
}
print("finished \(queryString)")
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}
}
Because that's in an Operation subclass, isCancelled is implicitly referencing itself rather than some ivar, avoiding any confusion about what it's checking. And your "start a new query" code can just say "cancel anything currently on the the relevant operation queue and add a new operation onto that queue":
private var searchQueue: OperationQueue = {
let queue = OperationQueue()
// queue.maxConcurrentOperationCount = 1 // make it serial if you want
queue.name = Bundle.main.bundleIdentifier! + ".backgroundQueue"
return queue
}()
func performSearch(for queryString: String) {
searchQueue.cancelAllOperations()
let operation = SearchOperation(queryString: queryString)
searchQueue.addOperation(operation)
}
I recommend this approach as you end up with a small cohesive object, the operation, that nicely encapsulates a block of work that you want to do, in the spirit of the Single Responsibility Principle.
While the following is less elegant, technically you can also use BlockOperation, which is block-based, but for which which you can decouple the creation of the operation, and the adding of the closure to the operation. Using this technique, you can actually pass a reference to the operation to its own closure:
private weak var lastOperation: Operation?
func performSearch(for queryString: String) {
lastOperation?.cancel()
let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
print("starting \(identifier)")
for i in 0 ... 10 {
if operation?.isCancelled ?? true { print("canceled \(identifier)"); return }
print(" \(identifier): \(i)")
self?.heavyWork()
}
print("finished \(identifier)")
}
searchQueue.addOperation(operation)
lastOperation = operation
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}
I only mention this for the sake of completeness. I think the Operation subclass approach is frequently a better design. I'll use BlockOperation for one-off sort of stuff, but as soon as I want more sophisticated cancelation logic, I think the Operation subclass approach is better.
I should also mention that, in addition to more elegant cancelation capabilities, Operation objects offer all sorts of other sophisticated capabilities (e.g. asynchronously manage queue of tasks that are, themselves, asynchronous; constrain degree of concurrency; etc.). This is all beyond the scope of this question.
you wrote
Ideally, I'd like to provide the newly-created DispatchWorkItem as an
additional parameter
you are wrong, to be able to cancel running task, you need a reference to it, not to the next which is ready to dispatch.
cancel() doesn't cancel running task, it only set internal "isCancel" flag by the thread-safe way, or remove the task from the queue before execution. Once executed, checking isCancel give you a chance to finish the job (early return).
import PlaygroundSupport
import Foundation
PlaygroundPage.current.needsIndefiniteExecution = true
let queue = DispatchQueue.global(qos: .background)
let prq = DispatchQueue(label: "print.queue")
var task: DispatchWorkItem?
func work(task: DispatchWorkItem?) {
sleep(1)
var d = Date()
if task?.isCancelled ?? true {
prq.async {
print("cancelled", d)
}
return
}
sleep(3)
d = Date()
prq.async {
print("finished", d)
}
}
for _ in 0..<3 {
task?.cancel()
let item = DispatchWorkItem {
work(task: task)
}
item.notify(queue: prq) {
print("done")
}
queue.asyncAfter(deadline: .now() + 0.5, execute: item)
task = item
sleep(1) // comment this line
}
in this example, only the very last job is really fully executed
cancelled 2018-12-17 23:49:13 +0000
done
cancelled 2018-12-17 23:49:14 +0000
done
finished 2018-12-17 23:49:18 +0000
done
try to comment the last line and it prints
done
done
finished 2018-12-18 00:07:28 +0000
done
the difference is, that first two execution never happened. (were removed from the dispatch queue before execution)
Reformed question
I have reformed my question. To the common case.
I want to generate items with RxSwift in background thread (loading from disk, long-running calculations, etc.), and observe items in MainThread. And I want to be sure that no items will be delivered after dispose (from main thread).
According to documentation (https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#disposing):
So can this code print something after the dispose call is executed? The answer is: it depends.
If the scheduler is a serial scheduler (ex. MainScheduler) and dispose is called on the same serial scheduler, the answer is no.
Otherwise it is yes.
But in case of using subscribeOn and observerOn with different schedulers - we cannot guarantee that nothing will be emitted after dispose (manual or by dispose bag, it does not matter).
How should I generate items (images, for example) in background and be sure that result will not be used after the dispose?
I made workaround in real project, but I want to solve this problem and to understand how should we avoid it in the same cases.
In my test project I have used small periods - they demonstrate the problem perfectly!
import RxSwift
class TestClass {
private var disposeBag = DisposeBag()
private var isCancelled = false
init(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
load(longRunningTaskDuration: longRunningTaskDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + cancelAfter) { [weak self] in
self?.cancel()
}
}
private func load(longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
// We set task not cancelled
isCancelled = false
DataService
.shared
.longRunngingTaskEmulation(sleepFor: longRunningTaskDuration)
// We want long running task to be executed in background thread
.subscribeOn(ConcurrentDispatchQueueScheduler.init(queue: .global()))
// We want to process result in Main thread
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { [weak self] (result) in
assert(Thread.isMainThread)
guard let strongSelf = self else {
return
}
if !strongSelf.isCancelled {
print("Should not be called! Task is cancelled!")
} else {
// Do something with result, set image to UIImageView, for instance
// But if task was cancelled, this method will set invalid (old) data
print(result)
}
}, onError: nil)
.disposed(by: disposeBag)
}
// Cancel all tasks. Can be called in PreapreForReuse.
private func cancel() {
assert(Thread.isMainThread)
// For test purposes. After cancel, old task should not make any changes.
isCancelled = true
// Cancel all tasks by creating new DisposeBag (and disposing old)
disposeBag = DisposeBag()
}
}
class DataService {
static let shared = DataService()
private init() { }
func longRunngingTaskEmulation(sleepFor: TimeInterval) -> Single<String> {
return Single
.deferred {
assert(!Thread.isMainThread)
// Enulate long running task
Thread.sleep(forTimeInterval: sleepFor)
// Return dummy result for test purposes.
return .just("Success")
}
}
}
class MainClass {
static let shared = MainClass()
private init() { }
func main() {
Timer.scheduledTimer(withTimeInterval: 0.150, repeats: true) { [weak self] (_) in
assert(Thread.isMainThread)
let longRunningTaskDuration: TimeInterval = 0.050
let offset = TimeInterval(arc4random_uniform(20)) / 1000.0
let cancelAfter = 0.040 + offset
self?.executeTest(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
}
}
var items: [TestClass] = []
func executeTest(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
let item = TestClass(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
items.append(item)
}
}
Call MainClass.shared.main() somewhere to start.
We call method to load some data and later we call cancel (all from Main Thread). After cancel we sometimes receive the result (in main thread too), but it is old already.
In real project TestClass is a UITableViewCell subclass and cancel method is called in prepareForReuse. Then cell is being reused and new data is set to the cell. And later we get the result of OLD task. And old image is set to the cell!
ORIGINAL QUESTION (OLD):
I would like to load image with RxSwift in iOS. I want to load image in background, and to use it in main thread. So I subscribeOn background thread, and observeOn main thread. And function will look like this:
func getImage(path: String) -> Single<UIImage> {
return Single
.deferred {
if let image = UIImage(contentsOfFile: path) {
return Single.just(image)
} else {
return Single.error(SimpleError())
}
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
}
But I get problems with cancelation. Because different schedulers are used to create items and to call dispose (disposing from main thread), subscription event can be raised after dispose is called. So in my case of using in UITableViewCell I receive invalid (old) image.
If I create item (load image) in the same scheduler that observes (Main thread), everything works fine!
But I would like to load images in background and I want it will be canceled after disposing (in prepareForReuse method or in new path set method). What is the common template for this?
EDIT:
I have created a test project, where I can emulate the problem when the event is received after dispose.
And I have one simple solution that works. We should emit items in the same scheduler. So we should capture scheduler and emit items there (after long running task completes).
func getImage2(path: String) -> Single<UIImage> {
return Single
.create(subscribe: { (single) -> Disposable in
// We captrure current queue to execute callback in
// TODO: It can be nil if called from background thread
let callbackQueue = OperationQueue.current
// For async calculations
OperationQueue().addOperation {
// Perform any long-running task
let image = UIImage(contentsOfFile: path)
// Emit item in captured queue
callbackQueue?.addOperation {
if let result = image {
single(.success(result))
} else {
single(.error(SimpleError()))
}
}
}
return Disposables.create()
})
.observeOn(MainScheduler.instance)
}
But it is not in Rx way. And I think this is not the best solution.
May be I should use CurrentThreadScheduler to emit items, but I cannot understand how. Is there any tutorial or example of items generation with schedulers usage? I did not find any.
Interesting test case. There is a small bug, it should be if strongSelf.isCancelled instead of if !strongSelf.isCancelled. Apart from that, the test case shows the problem.
I would intuitively expect that it is checked whether a dispose has already taken place before emitting, if it happens on the same thread.
I found additionally this:
just to make this clear, if you call dispose on one thread (like
main), you won't observe any elements on that same thread. That is a
guarantee.
see here: https://github.com/ReactiveX/RxSwift/issues/38
So maybe it is a bug.
To be sure I opened an issue here:
https://github.com/ReactiveX/RxSwift/issues/1778
Update
It seems it was actually a bug. Meanwhile, the fine people at RxSwift have confirmed it and fortunately fixed it very quickly. See the issue link above.
Testing
The bug was fixed with commit bac86346087c7e267dd5a620eed90a7849fd54ff. So if you are using CocoaPods, you can simply use something like the following for testing:
target 'RxSelfContained' do
use_frameworks!
pod 'RxAtomic', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
pod 'RxSwift', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
end
I'm using a UIProgressView and it uses the observedProgress property. I then have a variable of type Progress that is observed.
Now I'm writing to Core Data on a background thread and then updating the completedUnitCount but it's crashing.
Here's the code:
var downloadProgress: Progress
init() {
downloadProgress = Progress()
}
func saveStuff() {
let stuff: [[String: Any]] = //some array of dictionaries
downloadProgress.totalUnitCount = Int64(stuff.count)
persistentContainer.performBackgroundTask { (context) in
for (index, item) in stuff.enumerated() {
// create items to be saved
context.perform {
do {
try context.save()
self.downloadProgress.completedUnitCont = Int64(index + 1)
} catch {
// handle error
}
}
}
}
}
So it's crashing on line self.downloadProgress.completedUnitCont = Int64(index + 1). I realise in writing this that I should also be using weak or unowned self to stop retain cycles, but are there other issues?
All the UI related code have to be performed from the main thread, so you have to dispatch call self.downloadProgress.completedUnitCont = Int64(index + 1) to main thread. Something like this:
DispatchQueue.main.async {
self.downloadProgress.completedUnitCont = Int64(index + 1)
}
Apple Says:
Updating UI on a thread other than the main thread is a common mistake that can result in missed UI updates, visual defects, data corruptions, and crashes.
So whenever you are performing any task on background thread and need to make any ui updates in the process, use all those code inside the following block.
DispatchQueue.main.async{ <uiupdate code here> }
How can I prevent a block of code to be repeatedly accessed from the same thread?
Suppose, I have the next code:
func sendAnalytics() {
// some synchronous work
asyncTask() { _ in
completion()
}
}
I want to prevent any thread from accessing "// some synchronous work", before completion was called.
objc_sync_enter(self)
objc_sync_exit(self)
seem to only prevent accessing this code from multiple threads and don't save me from accessing this code from the single thread. Is there a way to do this correctly, without using custom solutions?
My repeatedly accessing, I mean calling this sendAnalytics from one thread multiple times. Suppose, I have a for, like this:
for i in 0...10 {
sendAnalytics()
}
Every next call won't be waiting for completion inside sendAnalytics get called (obvious). Is there a way to make the next calls wait, before completion fires? Or the whole way of thinking is wrong and I have to solve this problem higher, at the for body?
You can use a DispatchSemaphore to ensure that one call completes before the next can start
let semaphore = DispatchSemaphore(value:1)
func sendAnalytics() {
self.semaphore.wait()
// some synchronous work
asyncTask() { _ in
completion()
self.semaphore.signal()
}
}
The second call to sendAnalytics will block until the first asyncTask is complete. You should be careful not to block the main queue as that will cause your app to become non-responsive. It is probably safer to dispatch the sendAnalytics call onto its own serial dispatch queue to eliminate this risk:
let semaphore = DispatchSemaphore(value:1)
let analyticsQueue = DispatchQueue(label:"analyticsQueue")
func sendAnalytics() {
analyticsQueue.async {
self.semaphore.wait()
// some synchronous work
asyncTask() { _ in
completion()
self.semaphore.signal()
}
}
}