Swift handle lots of complex operations - ios

I'm working on a Swift project with complicated notification calculations. I'm looping through an array of objects which I need to do a very complex operation to determine when to schedule notifications. Currently I am doing:
for item in items {
//some logic here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
//complex operation here, then schedule notification
}
}
Unfortunately this is proving to be slow since I am doing this for a bunch of objects that are all trying to use the same queue. Sometimes it doesn't finish scheduling the notifications before the user leaves the app. What are my options to improve the performance? I was thinking instead of having everything use the global high priority queue I could create new queues each time somehow so they are not waiting on each other?

First, I don't think you need to use the "main thread" for calculation, the main thread is mainly for UI updates. You should use background thread to handle those heavy operations and schedule notifications. Otherwise you app is going to be very laggy and not responsive(as you main thread is occupied by those operations)
Second, in stead of firing multiple main thread operations using a for loop. You should put the code in this way.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
for item in items {
//some logic here
//complex operation here, then schedule notification
}
}
As you didnt provide code of the complex operation, can't give any advise on that part. Feel free to add follow up questions. :D

Related

ios/swift dispatch queues - global and main queue concepts

So if I were to do something heavy, and then update the UI, would this be the best way of doing it?
DispatchQueue.global().async {
// Do something heavy here, such as adding 10000 objects to an array
DispatchQueue.main.async {
// Update UI here after the heavy lifting is finished, such as tableView.reloadData()
}
}
Yes, this one of the ways you can use to avoid blocking the main thread , but there is many other alternatives such as using DispatchGroup for chaining asynchronous tasks ,OperationQueue or even create your own queue and inside it do heavy work and hand the UI things to main thread , but to keep in mind the global queue is only one queue if you find that there is a lot of calls to it , it's better to create a new helper one

How will UndoManager run loop grouping be affected in different threading contexts?

TLDR: I'm wondering how UndoManager automatic undo grouping based on run loops is effected when using from a background thread, and what my best option is for this.
I am using UndoManager (formerly NSUndoManager) in a custom Swift framework with targets for both iOS and macOS.
Within the framework, a decent amount of work takes place on background GCD serial queues. I understand that UndoManager automatically groups top-level registered undo actions per run loop cycle, but I am not sure how different threading situations would affect that.
My Questions:
What affect, if any, would the following situations have on UndoManagers run loop grouping of registered undo actions?
Which situation (other than situation 1, which is not feasible) is ideal to provide natural grouping assuming all changes that require undo registration will take place on a singular background serial dispatch queue?
In all below situations, assume methodCausingUndoRegistration() and anotherMethodCausingUndoRegistration() are nothing fancy and call UndoManager.registerUndo from the thread they were called on without any dispatch.
Situation 1: Inline on Main Thread
// Assume this runs on main thread
methodCausingUndoRegistration()
// Other code here
anotherMethodCausingUndoRegistration()
// Also assume every other undo registration in this framework takes place inline on the main thread
My Understanding: This is how UndoManager expects to be used. Both of the undo registrations above will take place in the same run loop cycle and therefore be placed in the same undo group.
Situation 2: Synchronous Dispatch on Main Thread
// Assume this runs on an arbitrary background thread, possibly managed by GCD.
// It is guaranteed not to run on the main thread to prevent deadlock.
DispatchQueue.main.sync {
methodCausingUndoRegistration()
}
// Other code here
DispatchQueue.main.sync {
anotherMethodCausingUndoRegistration()
}
// Also assume every other undo registration in this framework takes place
// by syncing on main thread first as above
My Understanding: Obviously, I would not want to use this code in production because synchronous dispatch is not a great idea in most situations. However, I suspect that it is possible for these two actions to get placed into separate run loop cycles based on timing considerations.
Situation 3: Asynchronous Dispatch on Main Thread
// Assume this runs from an unknown context. Might be the main thread, might not.
DispatchQueue.main.async {
methodCausingUndoRegistration()
}
// Other code here
DispatchQueue.main.async {
anotherMethodCausingUndoRegistration()
}
// Also assume every other undo registration in this framework takes place
// by asyncing on the main thread first as above
My Understanding: As much as I would like for this to produce the same effect as situation 1, I suspect it might be possible for this to cause similar undefined grouping as Situation 2.
Situation 4: Single Asynchronous Dispatch on Background Thread
// Assume this runs from an unknown context. Might be the main thread, might not.
backgroundSerialDispatchQueue.async {
methodCausingUndoRegistration()
// Other code here
anotherMethodCausingUndoRegistration()
}
// Also assume all other undo registrations take place
// via async on this same queue, and that undo operations
// that ought to be grouped together would be registered
// within the same async block.
My Understanding: I really hope this will act the same as Situation 1 as long as the UndoManager is used exclusively from this same background queue. However, I worry that there may be some factors that make the grouping undefined, especially since I don't think GCD queues (or their managed threads) always (if ever) get run loops.
TLDR: When working with UndoManager from a background thread, the least complex option is to simply disable automatic grouping via groupsByEvent and do it manually. None of the situations above will work as intended. If you really want automatic grouping in the background, you'd need to avoid GCD.
I'll add some background to explain expectations, then discuss what actually happens in each situation, based on experiments I did in an Xcode Playground.
Automatic Undo Grouping
The "Undo manager" chapter of Apple's Cocoa Application Competencies for iOS Guide states:
NSUndoManager normally creates undo groups automatically during a cycle of the run loop. The first time it is asked to record an undo operation in the cycle, it creates a new group. Then, at the end of the cycle, it closes the group. You can create additional, nested undo groups.
This behavior is easily observable in a project or Playground by registering ourself with NotificationCenter as an observer of NSUndoManagerDidOpenUndoGroup and NSUndoManagerDidCloseUndoGroup. By observing these notification and printing results to the console including undoManager.levelsOfUndo, we can see exactly what is going on with the grouping in real time.
The guide also states:
An undo manager collects all undo operations that occur within a single cycle of a run loop such as the application’s main event loop...
This language would indicate the main run loop is not the only run loop UndoManager is capable of observing. Most likely, then, UndoManager observes notifications that are sent on behalf of the CFRunLoop instance that was current when the first undo operation was recorded and the group was opened.
GCD and Run Loops
Even though the general rule for run loops on Apple platforms is 'one run loop per thread', there are exceptions to this rule. Specifically, it is generally accepted that Grand Central Dispatch will not always (if ever) use standard CFRunLoops with its dispatch queues or their associated threads. In fact, the only dispatch queue that seems to have an associated CFRunLoop seems to be the main queue.
Apple's Concurrency Programming Guide states:
The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread. This queue works with the application’s run loop (if one is present) to interleave the execution of queued tasks with the execution of other event sources attached to the run loop.
It makes sense that the main application thread would not always have a run loop (e.g. command line tools), but if it does, it seems it is guaranteed that GCD will coordinate with the run loop. This guarantee does not appear to be present for other dispatch queues, and there does not appear to be any public API or documented way of associated an arbitrary dispatch queue (or one of its underlying threads) with a CFRunLoop.
This is observable by using the following code:
DispatchQueue.main.async {
print("Main", RunLoop.current.currentMode)
}
DispatchQueue.global().async {
print("Global", RunLoop.current.currentMode)
}
DispatchQueue(label: "").async {
print("Custom", RunLoop.current.currentMode)
}
// Outputs:
// Custom nil
// Global nil
// Main Optional(__C.RunLoopMode(_rawValue: kCFRunLoopDefaultMode))
The documentation for RunLoop.currentMode states:
This method returns the current input mode only while the receiver is running; otherwise, it returns nil.
From this, we can deduce that Global and Custom dispatch queues don't always (if ever) have their own CFRunLoop (which is the underlying mechanism behind RunLoop). So, unless we are dispatching to the main queue, UndoManager won't have an active RunLoop to observe. This will be important for Situation 4 and beyond.
Now, let's observe each of these situations using a Playground (with PlaygroundPage.current.needsIndefiniteExecution = true) and the notification-observing mechanism discussed above.
Situation 1: Inline on Main Thread
This is exactly how UndoManager expects to be used (based on the documentation). Observing the undo notifications shows a single undo group being created with both undos inside.
Situation 2: Synchronous Dispatch on Main Thread
In a simple test using this situation, we get each of the undo registrations in its own group. We can therefore conclude that those two synchronously-dispatched blocks each took place in their own run loop cycle. This appears to always be the behavior dispatch sync produces on the main queue.
Situation 3: Asynchronous Dispatch on Main Thread
However, when async is used instead, a simple test reveals the same behavior as Situation 1. It seems that because both blocks were dispatched to the main thread before either had a chance to actually be run by the run loop, the run loop performed both blocks in the same cycle. Both undo registrations were therefore placed in the same group.
Based purely on observation, this appears to introduces a subtle difference in sync and async. Because sync blocks the current thread until done, the run loop must begin (and end) a cycle before returning. Of course, then, the run loop would not be able to run the other block in that same cycle because they would not have been there when the run loop started and looked for messages. With async, however, the run loop likely didn't happen to start until both blocks were already queued, since async returns before the work is done.
Based on this observation, we can simulate situation 2 inside situation 3 by inserting a sleep(1) call between the two async calls. This way, The run loop has a chance to begin its cycle before the second block is ever sent. This indeed causes two undo groups to be created.
Situation 4: Single Asynchronous Dispatch on Background Thread
This is where things get interesting. Assuming backgroundSerialDispatchQueue is a GCD custom serial queue, a single undo group is created immediately before the first undo registration, but it is never closed. If we think about our discussion above about GCD and run loops, this makes sense. An undo group is created simply because we called registerUndo and there was no top-level group yet. However, it was never closed because it never got a notification about the run loop ending its cycle. It never got that notification because background GCD queues don't get functional CFRunLoops associated with them, so UndoManager was likely never even able to observe the run loop in the first place.
The Correct Approach
If using UndoManager from a background thread is necessary, none of the above situations are ideal (other than the first, which does not meet the requirement of being triggered in the background). There are two options that seem to work. Both assume that UndoManager will only be used from the same background queue/thread. After all, UndoManager is not thread safe.
Just Don't Use Automatic Grouping
This automatic undo grouping based on run loops may easily be turned off via undoManager.groupsByEvent. Then manual grouping may be achieved like so:
undoManager.groupsByEvent = false
backgroundSerialDispatchQueue.async {
undoManager.beginUndoGrouping() // <--
methodCausingUndoRegistration()
// Other code here
anotherMethodCausingUndoRegistration()
undoManager.endUndoGrouping() // <--
}
This works exactly as intended, placing both registrations in the same group.
Use Foundation Instead of GCD
In my production code, I intend to simply turn off automatic undo grouping and do it manually, but I did discover an alternative while investigating the behavior of UndoManager.
We discovered earlier that UndoManager was unable to observe custom GCD queues because they did not appear to have associated CFRunLoops. But what if we created our own Thread and set up a corresponding RunLoop instead. In theory, this should work, and the code below demonstrates:
// Subclass NSObject so we can use performSelector to send a block to the thread
class Worker: NSObject {
let backgroundThread: Thread
let undoManager: UndoManager
override init() {
self.undoManager = UndoManager()
// Create a Thread to run a block
self.backgroundThread = Thread {
// We need to attach the run loop to at least one source so it has a reason to run.
// This is just a dummy Mach Port
NSMachPort().schedule(in: RunLoop.current, forMode: .commonModes) // Should be added for common or default mode
// This will keep our thread running because this call won't return
RunLoop.current.run()
}
super.init()
// Start the thread running
backgroundThread.start()
// Observe undo groups
registerForNotifications()
}
func registerForNotifications() {
NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidOpenUndoGroup, object: undoManager, queue: nil) { _ in
print("opening group at level \(self.undoManager.levelsOfUndo)")
}
NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidCloseUndoGroup, object: undoManager, queue: nil) { _ in
print("closing group at level \(self.undoManager.levelsOfUndo)")
}
}
func doWorkInBackground() {
perform(#selector(Worker.doWork), on: backgroundThread, with: nil, waitUntilDone: false)
}
// This function needs to be visible to the Objc runtime
#objc func doWork() {
registerUndo()
print("working on other things...")
sleep(1)
print("working on other things...")
print("working on other things...")
registerUndo()
}
func registerUndo() {
let target = Target()
print("registering undo")
undoManager.registerUndo(withTarget: target) { _ in }
}
class Target {}
}
let worker = Worker()
worker.doWorkInBackground()
As expected, the output indicates that both undos are placed in the same group. UndoManager was able to observe the cycles because the Thread was using a RunLoop, unlike GCD.
Honestly, though, it's probably easier to stick with GCD and use manual undo grouping.

Whether those two ways of dispatching work to main thread (CGD and NSOperationQueue) are equivalent?

I'm curious whether those two types to dispatch work to main queue are equivalent or maybe there are some differentials?
dispatch_async(dispatch_get_main_queue()) {
// Do stuff...
}
and
NSOperationQueue.mainQueue().addOperationWithBlock { [weak self] () -> Void in
// Do stuff..
}
There are differences, but they are somewhat subtle.
Operations enqueued to -[NSOperationQueue mainQueue] get executed one operation per pass of the run loop. This means, among other things, that there will be a "draw" pass between operations.
With dispatch_async(dispatch_get_main_queue(),...) and -[performSelectorOnMainThread:...] all enqueued blocks/selectors are called one after the other without spinning the run loop (i.e. allowing views to draw or anything like that). The runloop will continue after executing all enqueued blocks.
So, with respect to drawing, dispatch_async(dispatch_get_main_queue(),...) and -[performSelectorOnMainThread:...] batch operations into one draw pass, whereas -[NSOperationQueue mainQueue] will draw after each operation.
For a full, in-depth investigation of this, see my answer over here.
At a very basic level they are not both the same thing.
Yes, the operation queue method will be scheduled on GCD queue. But it also gets all the rich benefits of using operation queues, such as an easy way to add dependent operations; state observation; the ability to cancel an operation…
So no, they are not equivalent.
Yes there are difference in GCD and NSOperation.
GCD is light weight can be used to give flavor of multithreading like loading profile pic, loading web page, network call that surely returns at earliest.
NSOperation queue 1. Usually used to make heavy network calls, sort thousand's of record etc.2. Can add new operation, delete, get current status at any operation3. Add completion handler4. get operation count etc are added advantages over GCD

NSNotification - observer with multiple events to trigger

As it stands, NSNotifications allow for a target-action mechanism in response to one post / event.
I would like to have a notification which triggers an action (runs a function) only after two events have been triggered.
The scenario is that I have two asynchronous processes which need to complete before I can call the function. Perhaps I'm missing something, but I haven't found a way to do this. Or maybe I'm not thinking of an obvious reason why this would be a really bad idea?
Also, some of my terminology may be off, so please feel free to edit and fix it.
There are many possibilities on how you can implement this. They all center around keeping track of which processes are finished. The best way depends on how your background processes are implemented.
If you are using NSOperationQueue you could add a third operation that has the other two operations as a dependency. That way you won't have to take care of notifications at all.
Otherwise you can can count how many operations have finished and execute your code when the counter reaches the right value. GCD has dispatch groups as a nice abstraction for this.
First you create a dispatch group:
let group = dispatch_group_create()
Then you enter the group for each background process:
dispatch_group_enter(group)
Finally you can register an block that gets called when the group becomes empty, that is when each dispatch_group_enter is balanced by an dispatch_group_leave:
dispatch_group_notify(group, dispatch_get_main_queue()) {
// All processes are done.
}
After each of your processes finish you leave the group again:
dispatch_group_leave(group)
It's important to call dispatch_group_enter before calling dispatch_group_notify or your block will be scheduled immediately as the group is already empty.
After your notify block was executed you can reuse the queue or discard it.

#synchronized block versus GCD dispatch_async()

Essentially, I have a set of data in an NSDictionary, but for convenience I'm setting up some NSArrays with the data sorted and filtered in a few different ways. The data will be coming in via different threads (blocks), and I want to make sure there is only one block at a time modifying my data store.
I went through the trouble of setting up a dispatch queue this afternoon, and then randomly stumbled onto a post about #synchronized that made it seem like pretty much exactly what I want to be doing.
So what I have right now is...
// a property on my object
#property (assign) dispatch_queue_t matchSortingQueue;
// in my object init
_sortingQueue = dispatch_queue_create("com.asdf.matchSortingQueue", NULL);
// then later...
- (void)sortArrayIntoLocalStore:(NSArray*)matches
{
dispatch_async(_sortingQueue, ^{
// do stuff...
});
}
And my question is, could I just replace all of this with the following?
- (void)sortArrayIntoLocalStore:(NSArray*)matches
{
#synchronized (self) {
// do stuff...
};
}
...And what's the difference between the two anyway? What should I be considering?
Although the functional difference might not matter much to you, it's what you'd expect: if you #synchronize then the thread you're on is blocked until it can get exclusive execution. If you dispatch to a serial dispatch queue asynchronously then the calling thread can get on with other things and whatever it is you're actually doing will always occur on the same, known queue.
So they're equivalent for ensuring that a third resource is used from only one queue at a time.
Dispatching could be a better idea if, say, you had a resource that is accessed by the user interface from the main queue and you wanted to mutate it. Then your user interface code doesn't need explicitly to #synchronize, hiding the complexity of your threading scheme within the object quite naturally. Dispatching will also be a better idea if you've got a central actor that can trigger several of these changes on other different actors; that'll allow them to operate concurrently.
Synchronising is more compact and a lot easier to step debug. If what you're doing tends to be two or three lines and you'd need to dispatch it synchronously anyway then it feels like going to the effort of creating a queue isn't worth it — especially when you consider the implicit costs of creating a block and moving it over onto the heap.
In the second case you would block the calling thread until "do stuff" was done. Using queues and dispatch_async you will not block the calling thread. This would be particularly important if you call sortArrayIntoLocalStore from the UI thread.

Resources