Using dispatch queues in your application

A basic understanding of threads is good enough for you to start using them in your applications. However, once you start using them, chances are that they suddenly become confusing again. If this happens to you, don't worry; threading is not easy. Now, let's go ahead,  dive deep, and look at an example of threaded code:

var someBoolean = false

DispatchQueue(label: "MutateSomeBoolean").async {
    // perform some work here
    for i in 0..<100 {
        continue
    }
    
    someBoolean = true
}

print(someBoolean)

The preceding snippet demonstrates how you could mutate a global variable after performing a task that is too slow to execute on the main thread. We create an instance of a DispatchQueue and give it a label. This will create a new thread on which we can execute instructions. This queue represents the background thread from the visualization we looked at earlier.

Then, we call the async method on the DispatchQueue, and we pass it the closure that we want to execute on the queue we just created. The loop inside of this block is executed on the background thread; in the visualization, this would roughly compare to the fetch data and parse JSON instructions. Once the task is done, we mutate someBoolean.

The last line in the snippet prints the value of someBoolean. What do you think the value of someBoolean is at that point? If your answer is false, good job! If you thought true, you're not alone. A lot of people who start writing multithreaded, asynchronous code don't immediately grasp how it works exactly. Let's visualize the preceding snippet like we did with the networking example. Then, it will start to become clear what happened and why the value for someBoolean is false:

Because we're using a background thread, the main thread can immediately move to the next instruction. This means that the for loop and the print run simultaneously. In other words, we print someBoolean before it's mutated on the background thread. This is both the beauty and a caveat of using threads. When everything starts running simultaneously, it is hard to keep track of when something is completed.

The preceding visualization also exposed a potential problem in our code. We create a variable on the main thread and then we capture it in the background thread and mutate it there. Doing this is not recommended; your code could suffer from unintended side effects such as race conditions, where both the main thread and the background thread mutate a value, or worse, you could accidentally try to access a CoreData object on a different thread than the one it was created on. The CoreData objects do not support this, so you should always try to make sure that you avoid mutating or accessing objects that are not on the same thread as the one where you access them.

So, how can we mutate someBoolean safely and print its value after mutating it? Well, we could use a callback closure of our own. Let's see what this would look like:

func executeSlowOperation(withCallback callback: @escaping ((Bool) -  > Void)) {
    DispatchQueue(label: "MutateSomeBoolean").async {
        // perform some work here
        for i in 0..<100 {
            continue
        }
        
        callback(true)
    }
}

executeSlowOperation { result in
    DispatchQueue.main.async {
        someBoolean = result
        print(someBoolean)
    }
}

In this snippet, the slow operation is wrapped in a function that is called with a callback closure. Once the task is complete, the callback is executed and it is passed the resulting value. The closure makes sure that its code is executed on the main thread. If we don't do this, the closure itself would have been executed on the background thread. It's important to keep this in mind when calling your own asynchronous code.

The callback-based approach is great if your callback should be executed when a single task is finished. However, there are scenarios where you want to finish a number of tasks before moving over to the next task. We have already used this approach in Chapter 11, Being Proactive with Background Fetch. Let's review the heart of the background fetch logic that was used in that chapter:

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    
    let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest()
    let managedObjectContext = persistentContainer.viewContext
    guard let allMovies = try? managedObjectContext.fetch(fetchRequest) else {
        completionHandler(.failed)
        return
    }
    
    let queue = DispatchQueue(label: "movieDBQueue")
    let group = DispatchGroup()
    let helper = MovieDBHelper()
    var dataChanged = false
    
    for movie in allMovies {
        queue.async(group: group) {
            group.enter()
            helper.fetchRating(forMovieId: movie.remoteId) { id, 
popularity in guard let popularity = popularity, popularity !=
movie.popularity else { group.leave() return } dataChanged = true managedObjectContext.persist { movie.popularity = popularity group.leave() } } } } group.notify(queue: DispatchQueue.main) { if dataChanged { completionHandler(.newData) } else { completionHandler(.noData) } } }

When you first saw this code, you were probably able to follow along, but it's unlikely that you were completely aware of how complex this method really is. Multiple dispatch queues are used in this snippet. To give you an idea, this code begins on the main thread. Then, for each movie, a background queue is used to fetch its rating. Once the fetch is complete, the managed object context's dispatch queue is used to update the movie. Think about all this switching between dispatch queues that is going on for a second. Quite complex, isn't it?

The background fetch method needs to call a completion handler when it is done fetching all the data. However, we're using a lot of different queues, and it's kind of hard to tell when we're done with fetching everything. This is where dispatch groups come in. A dispatch group can hold on to a set of tasks that are executed either serially, or in parallel.

When you call enter() on a dispatch group, you are also expected to call leave() on the group. The enter call tells the group that there is unfinished work in the dispatch group. When you call leave(), the task is marked as completed. Once all tasks are completed, the group executes a closure on any thread you desire. In the example, the notify(queue:) is the method used to execute the completion handler on the main queue.

It's okay if this is a bit daunting or confusing right now. As mentioned before, asynchronous programming and threads are pretty complex topics, and dispatch groups are no different.

The most important takeaways regarding dispatch groups are that you call enter() on a group to submit an unfinished task. You call leave() to mark the task finished and, lastly, you use notify(queue:) to execute a closure on the queue passed to this method  once all tasks are marked completed.

The approach you've seen so far makes direct use of closures to perform tasks. This causes your methods to become long and fairly complex since everything is written in line with the rest of your code. You already saw how mixing code that exists on different threads can lead to confusion because it's not very obvious which code belongs on which queue. Also, all this inline code is not particularly reusable. We can't pick up a certain task and execute it on a different queue, for instance, because our code is already sort of tightly coupled to a certain dispatch queue.

In order to improve this situation, we should make use of Operations.