Chapter 21.  Offloading Tasks with Operations and GCD

The previous chapter showed you how you can use Instruments to measure your code's performance. This is a vital skill in discovering slow functions or memory leaks. You saw that sometimes it's easy to fix slow code and increase performance by simply fixing a programming error. However, the fix isn't always this easy. Some code simply can't be written quickly.

An example of this that you've already seen in this book, is networking. Whenever you fetch data from the network, you do so asynchronously. Not doing this would cause your code to hang for a lot longer than you'd want to, resulting in choppy scrolling and unresponsive interfaces.

Another example of slow code is loading an image from the app bundle and decoding the raw data into an image. For small images, this task shouldn't take as long as a networking request but imagine loading a couple of larger images. This would take longer than we'd want to and it would significantly damage your scrolling performance or render the interface unresponsive for longer than you should be comfortable with.

Luckily, we can easily make our code run asynchronously through dispatch queues. In Objective-C and early versions of Swift, this was mostly known as GCD. In Swift 3, the DispatchQueue class was introduced, improving both the APIs related to dispatch queues and simplifying their naming a bit.

This chapter will teach you how to make use of dispatch queues to write asynchronous code that can perform slow operations away from the main thread, meaning that they don't block the interface while they're running. Once you've got the hang of using dispatch queues, we'll look for reusable pieces of code and abstract them into Operations.

We'll cover the following topics in this chapter:

  • Writing asynchronous code with dispatch queues
  • Creating reusable tasks with Operations

By the end of this chapter you'll be able to enhance your apps by optimizing your code for asynchronous programming. You'll also be able to abstract certain tasks into Operations in order to make your app easier to understand and maintain.

Writing asynchronous code

In Chapter 10, Fetching and Displaying Data from the Network, we had our first run-in with asynchronous code and multithreading. We didn't go into too much detail regarding multithreading and asynchronous code because the subject of threading is rather complex and it's much more suited for a thorough discussion in this chapter than attempting to learn all about the subject as a side note.

If you're unclear about what exactly was explained already, feel free to skip back to Chapter 10, Fetching and Displaying Data from the Network to review the information presented there. The bottom line that you should understand is that networking is performed asynchronously on a different thread in order to avoid blocking the main thread. Once the network request is done, a callback is executed on the main thread so we can update the user interface.

In iOS, the main thread is arguably the most important thread you need to keep in mind. Let's see why.

Understanding threads

You've seen the term thread a couple of times now but we never really explored any of the details about threads. For instance, we never really looked at what a thread is or how we should mentally think about threads. This section aims to make the subject of threading a lot clearer to you so you can fully understand what a thread is and why they are such a vital part of building apps.

A good way to mentally think of a thread is a stack of instructions. In iOS, your app typically starts off with a single thread; the main thread. This thread is also known as the UI thread. It's called this because the main thread is where all of the UI elements are actually drawn, rendered, and pushed to the screen. All of this must happen on the same thread and if you think of a thread as a stack of instructions, you can see why it's important that we don't pollute this stack with instructions that take a long time because a thread can only execute a single instruction at a time:

Understanding threads

The preceding figure depicts a timeline where we would perform everything on the main thread. Notice how we can't update the interface until the fetch data and parse JSON instructions are completed. Also note that fetching data takes a lot longer than displaying the interface or handling a tap. During the fetch data instruction, the app will not be able to update any user interface elements or process any gestures or taps. This means that the app is practically frozen until the data is fetched and parsed.

Obviously, a good, responsive application can't afford to wait for slow instructions. The interface should always respond to user input, if this isn't the case, the app will feel slow, buggy, choppy, and just all round bad. This is where multithreading becomes interesting.

We can run multiple instruction stacks at the same time. Each stack is called a thread and we can execute certain instructions on a different thread to ensure that the interface remains responsive.

Understanding threads

This second figure depicts a more desirable scenario. The main thread only handles user interface-related tasks like handling taps, animating loaders, and scrolling. The background thread takes care of the tasks that don't relate to the user interface and could potentially take a while to finish.

By removing these instructions from the main thread and placing them on a different thread like iOS does by default for networking, we ensure that our app remains responsive even if the network requests take several seconds to finish or never finish at all.

Your app can utilize a large number of threads for different tasks. Do note that the number of threads isn't infinite so even if you use threading like you should, you should still make sure that you optimize your code as much as possible to avoid locking up several threads with slow code.

In the previous chapter, we used instruments to locate a piece of code that was slow. This resulted in an instruction on the main thread that took very long to complete, resulting in a frozen interface. Threading would not have solved this issue. The code that was slow was completely related to the user interface because we could not have rendered our collection without calculating the layout first. This is a scenario where it's extremely important to make sure that you write optimized code instead of simply relying on threads for anything that's slow.

Now that we've established an understanding of threads and how they can be utilized in your apps, let's have a look at how to manually offload tasks to different threads.

Using dispatch queues in your application

A basic understanding of threads is good enough for you to start using it in your applications. However, once you start using them, chances are that they suddenly become confusing again. Let's go ahead, deep dive 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 would mutate a global variable after performing a task too slow to execute on the main thread. We create an instance 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 dispatch queue 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's happening.

Using dispatch queues in your application

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 the caveat in using threads. When everything starts running simultaneously, it can be hard to keep track of when something is completed.

The visualization above 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 start suffering from unintended side effects like 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. 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 should 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, we wrapped our slow operation in a function that is called with a callback closure. Once the task is complete, we call the callback closure and pass it the resulting value. The closure makes sure that its code is executed on the main thread. If we didn'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 advanced this logic really is. We have a specific dispatch queue on which we start every movie update task. We immediately enter a new dispatch group due to the network request we're performing. Then we move back to the queue on which the managed object context was created to persist our updated movie. Think about this for a second and let it sink in before we look into the dispatch group that we used in addition to all of this dispatch queue switching.

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

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 can execute a closure on a given thread. In the example, the notify(queue:) is used to execute the completion handler on the main queue.

It's okay if this is a bit daunting or confusing right now. As stated 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 given queue 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 because 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset