Before we take a look at Swift Concurrency, it is important that you become familiar with a few core concepts that relate to Swift Concurrency. While you may already be familiar with some of them, this episode should take away any doubt or confusion you have.

Synchronous and Asynchronous Execution

We start with synchronous and asynchronous execution. Fire up Xcode and create a playground. Add an import statement for the Foundation framework at the top and define a URL for a remote image. We create a Data object by passing the URL object to the initializer of the Data struct. After creating the Data object, we print the number of bytes of the Data object to the console. To illustrate the difference between synchronous and asynchronous execution, we add a print statement that prints the string FINISHED.

import Foundation

let url = URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg")!

let data = try! Data(contentsOf: url)

print(data.count)

print("FINISHED")

Execute the contents of the playground and inspect the output in the console.

597628
FINISHED

The output visualizes the concept of synchronous execution. The statements in the playground are executed synchronously, from top to bottom. The benefit of synchronous execution is that the code is easy to follow and reason about. You simply read the statements from top to bottom.

The example also illustrates the most important drawback of synchronous execution. To create the Data object, the data for the remote image needs to be downloaded. That download operation takes time and the next statement, the statement printing the number of bytes of the Data object, isn't executed until the download operation is finished. This diagram illustrates why the print statement printing the string FINISHED is executed last. This may be acceptable in some situations, but it usually is not what you want. We talk more about that later.

Let's update the example to understand what asynchronous execution looks like. We use the URLSession API to download the data for the remote image. Don't worry about the syntax for now. You learn about tasks and the await keyword later in this series. What you need to understand for now is that we use a task to asynchronously execute a block of work. In this example, we download the data for the remote image and print the number of bytes of the Data object.

import Foundation

let url = URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg")!

Task {
    let (data, _) = try! await URLSession.shared.data(from: url)

    print(data.count)
}

print("FINISHED")

Execute the contents of the playground.

FINISHED
597628

The print statement printing the string FINISHED precedes the print statement printing the size of the Data object. The updated example changes the flow of the playground.

A task asynchronously executes a block of work. In other words, it takes a block of work and schedules it for asynchronous execution. This simply means that the block of work is executed independently of the main playground flow. The result is that the print statement printing the string FINISHED is executed before the download operation finishes.

To avoid confusion, let's use Grand Central Dispatch instead of a task. We ask Grand Central Dispatch for a reference to a global dispatch queue and pass a block of work to the dispatch queue's async(group:qos:flags:execute:) method. Like before, we use the URL object to create a Data object and print the number of bytes of the Data object to the console.

If you want to learn more about dispatch queues, then I recommend taking a look at Mastering Grand Central Dispatch. A dispatch queue manages the execution of blocks of work. That definition suffices to understand the example.

import Foundation

let url = URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg")!

DispatchQueue.global().async {
    let data = try! Data(contentsOf: url)

    print(data.count)
}

print("FINISHED")

Let's execute the contents of the playground.

FINISHED
597628

The output is identical to that of the previous example. The only difference is the technology we use. Like a task, a dispatch queue's async(group:qos:flags:execute:) method accepts a block of work and schedules it for asynchronous execution.

This diagram visualizes asynchronous execution. The download operation is executed independently of the main playground flow. The result is that the statement printing the string FINISHED precedes the statement printing the number of bytes of the Data object. The download operation doesn't block the main playground flow.

Serial and Concurrent Execution

The next concepts we explore are serial and concurrent execution. Let's take a look at another example to understand what these concepts mean. We define an array of URL objects. Each URL points to a remote image. We define two dispatch queues, a serial dispatch queue and a concurrent dispatch queue.

We iterate through the array of URL objects. In each iteration, we pass a block of work, a closure, to the async(group:qos:flags:execute:) method of the serial dispatch queue. In the closure, we use the URL object to create a Data object and we print the number of bytes of the Data object to the console. Note that we print STARTED DOWNLOADING and FINISHED DOWNLOADING to the console to track when the download operation started and when it finished.

import Foundation

let imageURLs = [
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/3.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/4.jpg")!
]

let serialDispatchQueue = DispatchQueue(label: "Serial Dispatch Queue")
let concurrentDispatchQueue = DispatchQueue(label: "Concurrent Dispatch Queue", attributes: .concurrent)

imageURLs.forEach { imageURL in
    serialDispatchQueue.async {
        print("STARTED DOWNLOADING")

        let data = try! Data(contentsOf: imageURL)

        print(data.count)

        print("FINISHED DOWNLOADING")
    }
}

Execute the contents of the playground. The output in the console shows that the data for the remote images is downloaded serially, one by one. The execution of the blocks of work is predictable and therefore easy to understand.

STARTED DOWNLOADING
2328941
FINISHED DOWNLOADING
STARTED DOWNLOADING
597628
FINISHED DOWNLOADING
STARTED DOWNLOADING
1260730
FINISHED DOWNLOADING
STARTED DOWNLOADING
1832795
FINISHED DOWNLOADING

This diagram visualizes serial execution. The blocks of work are added to the serial dispatch queue and scheduled for execution one by one. Serial execution guarantees that one block of work is executed at a time.

Let's replace the serial dispatch queue with the concurrent dispatch queue and execute the example one more time.

import Foundation

let imageURLs = [
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/3.jpg")!,
    URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/4.jpg")!
]

let serialDispatchQueue = DispatchQueue(label: "Serial Dispatch Queue")
let concurrentDispatchQueue = DispatchQueue(label: "Concurrent Dispatch Queue", attributes: .concurrent)

imageURLs.forEach { imageURL in
    concurrentDispatchQueue.async {
        print("STARTED DOWNLOADING")

        let data = try! Data(contentsOf: imageURL)

        print(data.count)

        print("FINISHED DOWNLOADING")
    }
}

The output in the console is quite different and the output I see may be different from the output you see. Why is that? Note that STARTED DOWNLOADING is printed to the console four consecutive times. This highlights the difference between serial and concurrent execution. A concurrent dispatch queue downloads the remote images concurrently or in parallel. The terms concurrent and in parallel are often used interchangeably. It simply means that the remote images are downloaded at the same time.

STARTED DOWNLOADING
STARTED DOWNLOADING
STARTED DOWNLOADING
STARTED DOWNLOADING
597628
FINISHED DOWNLOADING
1260730
FINISHED DOWNLOADING
2328941
FINISHED DOWNLOADING
1832795
FINISHED DOWNLOADING

Let's take a look at a diagram to understand what is happening. The blocks of work are added to the concurrent dispatch queue. The difference with serial execution is that the blocks of work are executed in parallel or concurrently. Multiple blocks of work are executed at the same time, which means that it takes less time to execute the blocks of work.

This change in execution has a number of consequences. The benefit of concurrent execution is speed or performance. By downloading the data for the remote images concurrently or in parallel, downloading the remote images takes less time. This is a significant benefit, but we also need to be mindful of an important caveat. The order of completion is unpredictable. As I mentioned earlier, the output you are seeing may differ from the output I am seeing. It is likely that the output changes if we execute the contents of the playground several times.

Serial and concurrent dispatch queues both honor the order in which blocks of work are scheduled, first in, first out, also known as FIFO. The order in which blocks of work complete is different. Because a serial dispatch queue executes one block at a time, the order of completion mirrors the order in which the blocks of work are scheduled. Serial execution is predictable and easy to reason about.

This isn't true for concurrent execution. Even though a concurrent dispatch queue honors the order in which the blocks of work are scheduled, the order of completion is unpredictable. That is what makes concurrent execution complex and, at times, hard to debug. Concurrent execution is a powerful concept, but keep in mind that it adds complexity.

What's Next?

We use the concepts we covered in this episode throughout the remainder of this series so take your time to understand them. In the next episode, we explore Swift Concurrency and why it is an important milestone for the Swift language. How does Swift Concurrency differ from Grand Central Dispatch? Why would you consider switching to Swift Concurrency? What problems does Swift Concurrency solve? We answer these questions in the next episode.