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

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.

Related

Does sync/async behave similar to serial/concurrent i.e., do they both control DispatchQueues or do sync/Async control Threads only

Most answers on stackoverflow implies in a way that sync vs async behaviour is quite similar to serial vs concurrent queue concept difference. Like the link in the first comment by #Roope
I have started to think that
Serial and concurrent are related to DispatchQueue, and sync/ async for how an operation will get executed on a thread.
Am I right?
Like if we've got DQ.main.sync then task/operation closure will get executed in a synchronous manner on this serial (main) queue.
And, if I do DQ.main.async then task will get asynchronously on some other background queue, and on reaching completion will return control on main thread.
And, since main is a serial queue, it won't let any other task/operation get into execution state/ start getting executed until the current closure task has finished its execution.
Then,
DQ.global().sync would execute a task synchronously on the thread on which its task/operation has been assigned i.e., it will block that thread from doing any other task/operation by blocking any context switching on that particular thread.
And, since, global is a concurrent queue it will keep on putting the tasks present in it to the execution state irrespective of previous task/operation's execution state.
DQ.global().async would allow context switching on the thread on which the operation closure has been put for execution
Is this the correct interpretations of the above dispatchQueues and sync vs async?
You are asking the right questions but I think you got a bit confused (mostly due to not very clear posts about this topic on internet).
Concurrent / Serial
Let's look at how you can create a new dispatch Queue:
let serialQueue = DispatchQueue(label: label)
If you don't specify any other additional parameter, this queue will behave as a serial queue:
This means that every block dispatched on this queue (sync or async it doesn't matter) will be executed alone, without the possibility for other blocks to be executed, on that same queue, simultaneously.
This doesn't mean that anything else is stopped, it just means that if something else is dispatched on that same queue, it will wait for the first block to finish before starting it's execution. Other threads and queues will still run on their own.
You can, however, create a concurrent queue, that will not constraint this blocks of code in this manner and, instead, if it happens that more blocks of code are dispatched on that same queue at the same time, it will execute them at the same time (on different threads)
let concurrentQueue = DispatchQueue(label: label,
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: .global())
So, you just need to pass the attribute concurrent to the queue, and it won't be serial anymore.
(I won't be talking about the other parameters since they are not in focus of this particular question and, I think, you can read about them in the other SO post linked in the comment or, if it's not enough, you can ask another question)
If you want to understand more about concurrent queues (aka: skip if you don't care about concurrent queues)
You could ask: When do I even need a concurrent queue?
Well, just for example, let's think of a use-case where you want to synchronize READS on a shared resource: since the reads can be done simultaneously without issues, you could use a concurrent queue for that.
But what if you want to write on that shared resource?
well, in this case a write needs to act as a "barrier" and during the execution of that write, no other write and no reads can operate on that resource simultaneously.
To obtain this kind of behavior, the swift code would look something like this
concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })
So, in other words, you can make a concurrent queue work temporarily as a serial queue in case you need.
Once again, the concurrent / serial distinction is only valid for blocks dispatched to that same queue, it has nothing to do with other concurrent or serial work that can be done on another thread/queue.
SYNC / ASYNC
This is totally another issue, with virtually no connection to the previous one.
This two ways to dispatch some block of code are relative to the current thread/queue you are at the time of the dispatch call. This dispatch call blocks (in case of sync) or doesn't block (async) the execution of that thread/queue while executing the code you dispatch on the other queue.
So let's say I'm executing a method and in that method I dispatch async something on some other queue (I'm using main queue but it could be any queue):
func someMethod() {
var aString = "1"
DispatchQueue.main.async {
aString = "2"
}
print(aString)
}
What happens is that this block of code is dispatched on another queue and could be executed serially or concurrently on that queue, but that has no correlation to what is happening on the current queue (which is the one on which someMethod is called).
What happens on the current queue is that the code will continue executing and won't wait for that block to be completed before printing that variable.
This means that, very likely, you will see it print 1 and not 2. (More precisely you can't know what will happen first)
If instead you would dispatch it sync, than you would've ALWAYS printed 2 instead of 1, because the current queue would've waited for that block of code to be completed, before continuing in it's execution.
So this will print 2:
func someMethod() {
var aString = "1"
DispatchQueue.main.sync {
aString = "2"
}
print(aString)
}
But does it mean that the queue on which someMethod is called is actually stopped?
Well, it depends on the current queue:
If it's serial, than yes. All the blocks previously dispatched to that queue or that will be dispatched on that queue will have to wait for that block to be completed.
If it's concurrent, than no. All concurrent blocks will continue their execution, only this specific block of execution will be blocked, waiting for this dispatch call to finish it's work. Of course if we are in the barriered case, than it's like for serial queues.
What happens when the currentQueue and the queue on which we dispatch are the same?
Assuming we are on serial queues (which I think will be most of your use-cases)
In case we dispatch sync, than deadlock. Nothing will ever execute on that queue anymore. That's the worst it could happen.
In case we dispatch async, than the code will be executed at the end of all the code already dispatched on that queue (including but not limited to the code executing right now in someMethod)
So be extra careful when you use the sync method, and be sure you are not on that same queue you are dispatching into.
I hope this let you understand better.
I have started to think that Serial and concurrent are related to DispatchQueue, and sync/async for how an operation will get executed on a thread.
In short:
Whether the destination queue is serial or concurrent dictates how that destination queue will behave (namely, can that queue run this closure at the same time as other things that were dispatched to that same queue or not);
Whereas sync vs async dictates how the current thread from which you are dispatching will behave (namely, should the calling thread wait until the dispatched code to finish or not).
So, serial/concurrent affects the destination queue to which you are dispatching, whereas sync/async affects the current thread from which you are dispatching.
You go on to say:
Like if we've got DQ.main.sync then task/operation closure will get executed in a synchronous manner on this serial (main) queue.
I might rephrase this to say “if we've got DQ.main.sync then the current thread will wait for the main queue to perform this closure.”
FWIW, we don’t use DQ.main.sync very often, because 9 times out of 10, we’re just doing this to dispatch some UI update, and there’s generally no need to wait. It’s minor, but we almost always use DQ.main.async. We do use sync is when we’re trying to provide thread-safe interaction with some resource. In that scenario, sync can be very useful. But it often is not required in conjunction with main, but only introduces inefficiencies.
And, if I do DQ.main.async then task will get asynchronously on some other background queue, and on reaching completion will return control on main thread.
No.
When you do DQ.main.async, you’re specifying the closure will run asynchronously on the main queue (the queue to which you dispatched) and that that your current thread (presumably a background thread) doesn’t need to wait for it, but will immediately carry on.
For example, consider a sample network request, whose responses are processed on a background serial queue of the URLSession:
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// parse the response
DispatchQueue.main.async {
// update the UI
}
// do something else
}
task.resume()
So, the parsing happens on this URLSession background thread, it dispatches a UI update to the main thread, and then carries on doing something else on this background thread. The whole purpose of sync vs async is whether the “do something else” has to wait for the “update the UI” to finish or not. In this case, there’s no point to block the current background thread while the main is processing the UI update, so we use async.
Then, DQ.global().sync would execute a task synchronously on the thread on which its task/operation has been assigned i.e., ...
Yes DQ.global().sync says “run this closure on a background queue, but block the current thread until that closure is done.”
Needless to say, in practice, we would never do DQ.global().sync. There’s no point in blocking the current thread waiting for something to run on a global queue. The whole point in dispatching closures to the global queues is so you don’t block the current thread. If you’re considering DQ.global().sync, you might as well just run it on the current thread because you’re blocking it anyway. (In fact, GCD knows that DQ.global().sync doesn’t achieve anything and, as an optimization, will generally run it on the current thread anyway.)
Now if you were going to use async or using some custom queue for some reason, then that might make sense. But there’s generally no point in ever doing DQ.global().sync.
... it will block that thread from doing any other task/operation by blocking any context switching on that particular thread.
No.
The sync doesn’t affect “that thread” (the worker thread of the global queue). The sync affects the current thread from which you dispatched this block of code. Will this current thread wait for the global queue to perform the dispatched code (sync) or not (async)?
And, since, global is a concurrent queue it will keep on putting the tasks present in it to the execution state irrespective of previous task/operation's execution state.
Yes. Again, I might rephrase this: “And, since global is a current queue, this closure will be scheduled to run immediately, regardless of what might already be running on this queue.”
The technical distinction is that when you dispatch something to a concurrent queue, while it generally starts immediately, sometimes it doesn’t. Perhaps all of the cores on your CPU are tied up running something else. Or perhaps you’ve dispatched many blocks and you’ve temporarily exhausted GCD’s very limited number of “worker threads”. Bottom line, while it generally will start immediately, there could always be resource constraints that prevent it from doing so.
But this is a detail: Conceptually, when you dispatch to a global queue, yes, it generally will start running immediately, even if you might have a few other closures that you have dispatched to that queue which haven’t finished yet.
DQ.global().async would allow context switching on the thread on which the operation closure has been put for execution.
I might avoid the phrase “context switching”, as that has a very specific meaning which is probably beyond the scope of this question. If you’re really interested, you can see WWDC 2017 video Modernizing Grand Central Dispatch Usage.
The way I’d describe DQ.global().async is that it simply “allows the current thread to proceed, unblocked, while the global queue performs the dispatched closure.” This is an extremely common technique, often called from the main queue to dispatch some computationally intensive code to some global queue, but not wait for it to finish, leaving the main thread free to process UI events, resulting in more responsive user interface.

What is the default thread

In iOS, we have GCD and Operation to handle concurrent programming.
looking into GCD we have QoS classes, and they're simple and straight forward, this question is about why DispatchQueue.main.async is commonly used to asynchronies X tasks in the Main Thread.
So when we usually handle updating something in the UI we usually use that function since to prevent any irresponsiveness from the application.
makes me think is writing code inside the UIViewController usually executed in the main thread ?
but also knowing that callback & completionHandler usually execute without specifying on what thread they are in, and the UI never had a problem with that !! so it is on the background ?
How Swift handles this ? and what thread am i writing on by default without specifying anything ?
Since there are more than one question here, let's attempt to answer them one by one.
why DispatchQueue.main.async is commonly used to asynchronies X tasks
in the Main Thread.
Before mentioning a direct answer, make sure that you don't have confusion of understanding:
Serial <===> Concurrent.
Sync <===> Async.
Keep in mind that DispatchQueue.main is serial queue. Using sync or async has nothing to do with determining serialization or currency of a queue, instead they refer to how the task is handled. Thus saying DispatchQueue.main.async means that:
returns control to the current queue right after task has been sent to
be performed on the different queue. It doesn't wait until the task is
finished. It doesn't block the queue.
cited from: https://stackoverflow.com/a/44324968/5501940 (I'd recommend to check it.)
In other words, async means: this will happen on the main thead and update it when it is finished. That's what makes what you said:
So when we usually handle updating something in the UI we usually use
that function since to prevent any irresponsiveness from the
application.
seems to be sensible; Using sync -instead of async- will block the main.
makes me think is writing code inside the UIViewController usually
executed in the main thread ?
First of all: By default, without specifying which thread should execute a chunk of code it would be the main thread. However your question seems to be unspecific because inside a UIViewController we can call functionalities that are not executed on the main thread by specifying it.
but also knowing that callback & completionHandler usually execute
without specifying on what thread they are in, and the UI never had a
problem with that !! so it is on the background ?
"knowing that callback & completionHandler usually execute without specifying on what thread they are in" No! You have to specify it. A good real example for it, actually that's how Main Thread Checker works.
I believe that there is something you are missing here, when dealing when a built-in method from the UIKit -for instance- that returns a completion handler, we can't see that it contains something like DispatchQueue.main.async when calling the completion handler; So, if you didn't execute the code inside its completion handler inside DispatchQueue.main.async so we should assume that it handles it for you! It doesn't mean that it is not implemented somewhere.
Another real-world example, Alamofire! When calling
Alamofire.request("https://httpbin.org/get").responseJSON { response in
// what is going on here work has to be async on the main thread
}
That's why you can call it without facing any "hanging" issue on the main thread; It doesn't mean its not handled, instead it means they handle it for you so you don't have to worry about it.

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

is there a way that the synchronized keyword doesn't block the main thread

Imagine you want to do many thing in the background of an iOS application but you code it properly so that you create threads (for example using GCD) do execute this background activity.
Now what if you need at some point to write update a variable but this update can occur or any of the threads you created.
You obviously want to protect that variable and you can use the keyword #synchronized to create the locks for you but here is the catch (extract from the Apple documentation)
The #synchronized() directive locks a section of code for use by a
single thread. Other threads are blocked until the thread exits the
protected code—that is, when execution continues past the last
statement in the #synchronized() block.
So that means if you synchronized an object and two threads are writing it at the same time, even the main thread will block until both threads are done writing their data.
An example of code that will showcase all this:
// Create the background queue
dispatch_queue_t queue = dispatch_queue_create("synchronized_example", NULL);
// Start working in new thread
dispatch_async(queue, ^
{
// Synchronized that shared resource
#synchronized(sharedResource_)
{
// Write things on that resource
// If more that one thread access this piece of code:
// all threads (even main thread) will block until task is completed.
[self writeComplexDataOnLocalFile];
}
});
// won’t actually go away until queue is empty
dispatch_release(queue);
So the question is fairly simple: How to overcome this ? How can we securely add a locks on all the threads EXCEPT the main thread which, we know, doesn't need to be blocked in that case ?
EDIT FOR CLARIFICATION
As you some of you commented, it does seem logical (and this was clearly what I thought at first when using synchronized) that only two the threads that are trying to acquire the lock should block until they are both done.
However, tested in a real situation, this doesn't seem to be the case and the main thread seems to also suffer from the lock.
I use this mechanism to log things in separate threads so that the UI is not blocked. But when I do intense logging, the UI (main thread) is clearly highly impacted (scrolling is not as smooth).
So two options here: Either the background tasks are too heavy that even the main thread gets impacted (which I doubt), or the synchronized also blocks the main thread while performing the lock operations (which I'm starting reconsidering).
I'll dig a little further using the Time Profiler.
I believe you are misunderstanding the following sentence that you quote from the Apple documentation:
Other threads are blocked until the thread exits the protected code...
This does not mean that all threads are blocked, it just means all threads that are trying to synchronise on the same object (the _sharedResource in your example) are blocked.
The following quote is taken from Apple's Thread Programming Guide, which makes it clear that only threads that synchronise on the same object are blocked.
The object passed to the #synchronized directive is a unique identifier used to distinguish the protected block. If you execute the preceding method in two different threads, passing a different object for the anObj parameter on each thread, each would take its lock and continue processing without being blocked by the other. If you pass the same object in both cases, however, one of the threads would acquire the lock first and the other would block until the first thread completed the critical section.
Update: If your background threads are impacting the performance of your interface then you might want to consider putting some sleeps into the background threads. This should allow the main thread some time to update the UI.
I realise you are using GCD but, for example, NSThread has a couple of methods that will suspend the thread, e.g. -sleepForTimeInterval:. In GCD you can probably just call sleep().
Alternatively, you might also want to look at changing the thread priority to a lower priority. Again, NSThread has the setThreadPriority: for this purpose. In GCD, I believe you would just use a low priority queue for the dispatched blocks.
I'm not sure if I understood you correctly, #synchronize doesn't block all threads but only the ones that want to execute the code inside of the block. So the solution probably is; Don't execute the code on the main thread.
If you simply want to avoid having the main thread acquire the lock, you can do this (and wreck havoc):
dispatch_async(queue, ^
{
if(![NSThread isMainThread])
{
// Synchronized that shared resource
#synchronized(sharedResource_)
{
// Write things on that resource
// If more that one thread access this piece of code:
// all threads (even main thread) will block until task is completed.
[self writeComplexDataOnLocalFile];
}
}
else
[self writeComplexDataOnLocalFile];
});

Context switches on iOS using "performSelectorOnMainThread"

I have weird threading issues on iOS. Consider a simple method like this:
- (BOOL)doSomething
{
[self doA];
[self doB];
}
This method should only run in context of the main thread. I need to call this method from a worker thread. I use performSelectorOnMainThread for this purpose.
If I do everything as explained here. Can a context switch happen between doA and `doB?
(I don't think so, I just want to make sure that my understanding is right here)
By "context switch" I assume you mean the main thread switching to some other main thread event (as you could always switch to another worker thread at any time).
However, main thread will finish all of doSomething before doing anything else.
Excerpt from performSelectorOnMainThread:
This method queues the message on the run loop of the main thread
using the common run loop modes—that is, the modes associated with the
NSRunLoopCommonModes constant. As part of its normal run loop
processing, the main thread dequeues the message (assuming it is
running in one of the common run loop modes) and invokes the desired
method. Multiple calls to this method from the same thread cause the
corresponding selectors to be queued and performed in the same same
order in which the calls were made.

Resources