Sync VS Async Queue in iOS

What's the difference between async and sync in swift

Both snippets append a closure to the queue.

But there is one difference.

Sync

Synch will wait for the closure to be executed before processing the next line.

So in this case, the print("Hello") is executed always after the closure.

queue.sync {
//some code
}
print("Hello")

Async

In this case the closure is added to the queue and then the next line is executed. So the print("Hello") could be executed before the closure.

queue.async {
//some code
}
print("Hello")

Difference between DispatchQueue.main.async and DispatchQueue.main.sync

When you use async it lets the calling queue move on without waiting until the dispatched block is executed. On the contrary sync will make the calling queue stop and wait until the work you've dispatched in the block is done. Therefore sync is subject to lead to deadlocks. Try running DispatchQueue.main.sync from the main queue and the app will freeze because the calling queue will wait until the dispatched block is over but it won't be even able to start (because the queue is stopped and waiting)

When to use sync? When you need to wait for something done on a DIFFERENT queue and only then continue working on your current queue

Example of using sync:

On a serial queue you could use sync as a mutex in order to make sure that only one thread is able to perform the protected piece of code at the same time.

for a serial queue, sync and async will give same output ? as there is only 1 thread available

looks like it will be giving same behavior on sync and async.

Yes, the behavior of the serial queue, itself, with respect to other items in that queue, doesn’t change depending upon whether you use sync or async. The only difference is the relationship between the current thread and the dispatch queue’s thread.

Consider:

queue.sync { print("A") }
queue.sync { print("B") }
queue.sync { print("C") }
queue.sync { print("D") }
print("E")

That will produce:

A
B
C
D
E

Compare that to:

queue.async { print("A") }
queue.async { print("B") }
queue.async { print("C") }
queue.async { print("D") }
print("E")

Because the current thread won’t wait for the tasks dispatched asynchronously to the serial queue, that will produce:

E
A
B
C
D

So, the behavior of A-D, all running on the queue, is unchanged. The only question is where that falls in comparison to E, generated by the the current thread. (FWIW, in this asynchronous example, E could actually happen anywhere before, in the middle of, or after A-D, but in practice, you'll see it run before the other queue gets to the asynchronously dispatched A-D.)

Sync vs Async Queue in iOS

Since you're using the same queue for both works, the second async block won't start executing until the first block ends. It doesn't matter if it's asynchronous or serial.

You'll see the true difference between .async and .sync if you add a print statement between both queues. Like this:

queue.async {
for _ in 1...100 {
self.smile()
}
}
print("Finished printing smiles")
queue.async {
for _ in 1...100 {
self.love()
}
}

The previous code will probably print "Finished printing smiles" even before it has started printing smiles! That's because async work returns instantly and keeps executing code.


And let's see what happens if you change the queues with synchronous queues:

queue.sync {
for _ in 1...100 {
self.smile()
}
}
print("Finished printing smiles")
queue.sync {
for _ in 1...100 {
self.love()
}
}

Yup. Now the sync queue waits before the closure completes before carrying on. So, you'll get 100 smiles, and then the "Finished printing smiles".


If you want to achieve concurrency, that's it, two blocks of code executing simultaneously (but not at exactly the same time, because that would be parallelism), you'll have to use two different queues, or specify the .concurrent parameter in the queue configuration:

override func viewDidLoad() {

let queue = DispatchQueue(label: "SerialQueue")
let queue2 = DispatchQueue(label: "AnotherQueue")

queue.async {
for _ in 1...100 {
self.smile()
}
}
print("Finished printing smiles")
queue2.async {
for _ in 1...100 {
self.love()
}
}
}

As you'll see, the order here is chaotic and will vary between executions. That's because both queues are running at the same time.

Another equivalent to this code would be:

let queue = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent)

queue.async {
for _ in 1...100 {
self.smile()
}
}
print("Finished printing smiles")
queue.async {
for _ in 1...100 {
self.love()
}
}

Difference Between DispatchQueue.sync vs DispatchQueue.async

Sync will stop the current thread until it has finished the task you are assigning to it.

Async will continue with the current thread and will execute the task in parallel or after the current thread.

Why it has unexpected behaviour?

That is because loadView() expects to have a UIView assigned to the view property after it has been executed, which you are doing it with async, which will be executed after loadView finishes.

The exception might be because you are not assigning a UIView on time or because you are handling the UI in your private Queue. UI should always be handled in the main thread.

Your variable que is a private queue, and because you didn't specify otherwise it is pointing to a background thread.

Editing your code like this might help you:

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white

let que = DispatchQueue.init(label: "testing")
// Executed 3 times
que.sync {
for i in 0...10 {
print(i)
}
}
// Giving me NSException
DispatchQueue.main.async {
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black

view.addSubview(label)

print("Label Added to Text View")
}
self.view = view
}
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Why concurrent queue with sync act like serial queue?

If you want to demonstrate them running concurrently, you should dispatch the 10 tasks individually:

let cq = DispatchQueue(label: "downloadQueue", attributes: .concurrent)

for i in 0..<10 {
cq.async {
sleep(2)
print(i)
}
}
print("all finished queuing them!")

Note:

  • There are 10 dispatches to the concurrent queue, not one.

    Each dispatched task runs concurrently with respect to other tasks dispatched to that queue (which is why we need multiple dispatches to illustrate the concurrency).

  • Also note that we dispatch asynchronously because we do not want to calling queue to wait for each dispatched task before dispatching the next.


You ask:

So my original thought was: the 1-10 printing should be done concurrently, not necessarily in the serial order.

Because they are inside a single dispatch, they will run as a single task, running in order. You need to put them in separate dispatches to see them run concurrently.

You go on to ask:

Could anyone explain the purpose of sync call on concurrent queue and give me an example why and when we need it?

The sync has nothing to do with whether the destination queue is serial or concurrent. The sync only dictates the behavior of the calling thread, namely, should the caller wait for the dispatched task to finish or not. In this case, you really do not want to wait, so you should use async.

As a general rule, you should avoid calling sync unless (a) you absolutely have to; and (b) you are willing to have the calling thread blocked until the sync task runs. So, with very few exceptions, one should use async. And, perhaps needless to say, we never block the main thread for more than a few milliseconds.

While using sync on a concurrent dispatch queue is generally avoided, one example you might encounter is the “reader-writer” synchronization pattern. In this case, “reads” happen synchronously (because you need to wait the result), but “writes” happen asynchronously with a barrier (because you do not need to wait, but you do not want it to happen concurrently with respect to anything else on that queue). A detailed discussion of using GCD for synchronization (esp the reader-writer pattern), is probably beyond the scope of this question. But search the web or StackOverflow for “GCD reader-writer” and you will find discussions on the topic.)


Let us graphically illustrate my revamped rendition of your code, using OSLog to create intervals in Instruments’ “Points of Interest” tool:

import os.log

private let log = OSLog(subsystem: "Foo", category: .pointsOfInterest)

class Foo {
func demonstration() {
let queue = DispatchQueue(label: "downloadQueue", attributes: .concurrent)

for i in 0..<10 {
queue.async { [self] in
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "async", signpostID: id, "%d", i)
spin(for: 2)
os_signpost(.end, log: log, name: "async", signpostID: id)
}
}
print("all finished queuing them!")
}

func spin(for seconds: TimeInterval) {
let start = CACurrentMediaTime()
while CACurrentMediaTime() - start < seconds { }
}
}

When I profile this in Instruments (e.g. “Product” » “Profile”), choosing “Time Profiler” template (which includes “Points of Interest” tool), I see a graphical timeline of what is happening:

Sample Image

So let me draw your attention to two interesting aspects of the above:

  1. The concurrent queue runs tasks concurrently, but because my iPhone’s CPU only has six cores, only six of them can actually run at the same time. The next four will have to wait until a core is available for that particular worker thread.

    Note, this demonstration only works because I am not just calling sleep, but rather I am spinning for the desired time interval, to more accurately simulate some slow, blocking task. Spinning is a better proxy for slow synchronous task than sleep is.

  2. This illustrates, as you noted, concurrent tasks may not appear in the precise order that they were submitted. This is because (a) they all were queued so quickly in succession; and (b) they run concurrently: There is a “race” as to which concurrently running thread gets to the logging statements (or “Points of Interest” intervals) first.

    Bottom line, for those tasks running concurrently, because of races, they may not appear to run in order. That is how concurrent execution works.

Why calling `DispatchQueue.main.sync` asynchronously from concurrent queue succeeds but synchronously fails?

.sync means it will block currently working thread, and wait until the closure has been executed. So your first .sync will block the main thread (you must be executing the .sync in the main thread otherwise it won't be deadlock). And wait until the closure in background.sync {...} has been finished, then it can continue.

But the second closure blocks the background thread and assign a new job to main thread, which has been blocked already. So these two threads are waiting for each other forever.

But if you switch your start context, like start your code in a background thread, could resolve the deadlock.


// define another background thread
let background2 = DispatchQueue(label: "backgroundQueue2",
qos: .background,
attributes: [],
autoreleaseFrequency: .inherit,
target: nil)
// don't start sample code in main thread.
background2.async {
background.sync {
DispatchQueue.main.sync {
print("Hello from background sync")
}
}
}

These deadlock is caused by .sync operation in a serial queue. Simply call DispatchQueue.main.sync {...} will reproduce the problem.

// only use this could also cause the deadlock.
DispatchQueue.main.sync {
print("Hello from background sync")
}

Or don't block the main thread at the very start could also resolve the deadlock.

background.async {
DispatchQueue.main.sync {
print("Hello from background sync")
}
}

Conclusion

.sync operation in a serial queue could cause permanent waiting because it's single threaded. It can't be stopped immediately and looking forward to a new job. The job it's doing currently should be done by first, then it can start another. That's why .sync could not be used in a serial queue.



Related Topics



Leave a reply



Submit