avatarRiccardo Cipolleschi

Summary

This context provides an in-depth explanation of concurrent development in iOS, focusing on the use of OperationQueue to handle complex flows.

Abstract

The content discusses the importance of concurrent programming in iOS app development, particularly when dealing with tasks like image processing and applying filters. It introduces OperationQueue as an abstraction that allows developers to think of their code as units of work, or Operations, that can be scheduled for asynchronous execution and run concurrently. The article explains how to configure and work with OperationQueue, including common settings such as maxConcurrentOperationCount, qualityOfService, and isSuspended. It also compares OperationQueue with DispatchQueue, highlighting the advantages and use cases of each.

Opinions

  • OperationQueue is a powerful tool for managing asynchronous and concurrent code in iOS development.
  • Concurrent programming is a crucial aspect of app development, as it allows for more efficient processing of complex tasks.
  • OperationQueue offers more control and management features than DispatchQueue, making it ideal for complex and fine-tuned operations.
  • Cancellation, priority, and dependencies are important features of OperationQueue that make it suitable for orchestrating different activities within an app.
  • OperationQueue will continue to be relevant and useful, even with the introduction of Combine and async/await.
  • Choosing the right tool (OperationQueue, DispatchQueue, Combine, or async/await) depends on the specific needs and requirements of the task at hand.
  • Proper management of resources and efficient task scheduling can significantly improve app performance and user experience.

Understanding Concurrent Development in iOS

Handle complex flows with OperationQueue

Photo by JuniperPhoton on Unsplash.

Concurrency and asynchronicity are two big topics in any app. We use asynchronous code every day for network requests and other operations, and we need to master it.

Concurrent programming is a more hidden topic, but it can pop up in any app as well. What if we develop an image-processing app and we want to apply different filters to different images at the same time to create a collage? With concurrent development, we don’t have to wait for all the filtering to complete sequentially. If we apply all the filters at the same time, we can wait only for the slowest one. This topic is so important that Apple provides a Concurrency Programming Guide.

Today, we will explore how to work with one of the most important abstractions to develop asynchronous and concurrent code: the OperationQueue.

What Is an OperationQueue?

An OperationQueue is an abstraction that lets us think about our code as a unit of work called an Operation. We can schedule them for asynchronous execution and run multiple operations at the same time. It is the operating system that decides when our operations can run.

This decision is taken by considering two aspects: how many resources are available and a bunch of settings we can use to configure both the OperationQueue and the Operation itself. Common settings are:

  • OperationQueue.maxConcurrentOperationCount: The number of operations that the OperationQueue can run at the same time. By default, it is set to defaultMaxConcurrentOperationCount and that value is determined by iOS.

Note: We can set the maxConcurrentOperationCount to 1 to implement a serial queue. This is a useful synchronization mechanism when we have to operate on a shared resource and we don’t want to handle multi-threading, lock, and semaphores manually!

  • OperationQueue.qualityOfService: This value is used as the default quality of service (QoS) value for all the operations we enqueue — when these operations do not specify a QoS already. Operations with a high QoS are executed before operations with a lower QoS.
  • OperationQueue.isSuspended: Whether the OperationQueue is suspended or not. When in a suspended state, we can enqueue operations, but they are not executed.
  • Operation.isReady: Whether the operation is ready to be executed.
  • Operation.isCancelled: Whether the operation is canceled or not. When an enqueued operation becomes the next operation to be executed, the OperationQueue checks if it is canceled. If it is, the queue will not execute the operation.
  • Operation.queuePriority: This value is used together with the QoS of a service to determine which operation can run. When there are multiple operations with the same QoS ready to be executed, the OperationQueue checks the queue priority and chooses the operation with the highest value.
  • Operation.dependencies: This array contains a set of operations that has to complete before the current operation can run. For example, we can define a combine operation that depends on other filter operations. The combine operation has to wait for all the others before it can run.

Working with the OperationQueue

Working with an OperationQueue is simple. We create a new queue using the default initializer. We can set a few properties for its general behavior and a name for it. The name can be useful to distinguish a queue from another.

By default, an OperationQueue is not suspended. As soon as a new operation is inserted, the queue will start executing it if it has enough resources. Once the queue is created, we can start enqueuing operations. We can choose between two different types of operations: BlockOperation or a custom implementation of the Operation class.

BlockOperation is the simplest type. We can add a BlockOperation by using the addOperation(_ block: @escaping () -> Void) overload. BlockOperations do not support QoS, priority, or dependencies. They can be canceled by using the methods of the OperationQueue, but once started, there is nothing we can do to stop them.

These simplified operations are very useful when we don’t need much control over them and we would like to run some code at some convenient time in an asynchronous way.

On the other hand, the Operation is an abstract class we can use as a parent for our custom operation. We can define as many subclasses as we want and submit all of them to the same OperationQueue.

To implement the subclass, we have to choose between two execution modes: synchronous and asynchronous execution. These are their differences:

  • Synchronous operations require us to implement the main() function of the Operation class. The queue may have to wait until the main method returns before starting another operation if it has reached the maximum number of concurrent operations.
  • Asynchronous operations require us to implement the start() method together with a bunch of other variables like isAsynchronous, isExecuting, and isFinished. The start method has to initiate an asynchronous operation, like a network call. It is our responsibility to handle the Operation state, updating isExecuting, isFinished, and the other state-related properties.

If our use case allows for task cancellation, we need to use a standard Operation. Handling the cancellation properly is one of our duties as concurrent developers. The typical approach is to check for the cancellation state at different points of the main() execution and terminate the operation if it has been cancelled. If the operation becomes cancelled at any point, we should roll back all the changes performed by the operation.

OperationQueue vs. DispatchQueue

OperationQueue and DispatchQueue are relatives. I’m pretty sure that you have used DispatchQueues several times. DispatchQueue.main.async or DispatchQueue.global().async are familiar pieces of code to move the execution to the proper thread.

There is nothing bad about that approach. Most of the time, we would still need to access the main queue to update the UI of our app. However, the dispatch queues have some limitations. This article makes it clear that we do not have much control over the dispatch queues. Once a task is submitted to them, it’s gone. If it does not terminate or if it goes in an endless loop, we cannot stop it and we are wasting resources. When such a thing happens, we lose access to the task completely. We cannot explore on which thread it is running, check for its state, or cancel it.

However, with an OperationQueue, we can always access the operations array and check for the state of the enqueued operations, check for the ones that are running, and eventually stop them.

As a rule of thumb:

  • If you need to dispatch some work in the main thread, use DispatchQueue.main.
  • If you have to dispatch a few tasks sporadically and are not interested in their management, you can use DispatchQueue.global().
  • If the app relies a lot on asynchronous operations, but you are not interested in fine management, use an OperationQueue with BlockOperations.
  • If the app needs to orchestrate different and complex activities, with cancellation, priority, and other fancy features, use an OperationQueue with some custom Operations.

Conclusion

In today’s article, we explored the main concepts of OperationQueue and how we can leverage this abstraction to schedule chunks of work on different threads.

We have studied the kinds of operations they support and how we can customize them. Finally, we analyzed the differences between OperationQueue and DispatchQueue.

An important note: OperationQueue won’t die with Combine and it won’t die with async/await either. Although they can look like similar mechanisms, they can be used for very different purposes. For example, we could already use some of Combine’s publishers within a BlockOperation to achieve complex results.

As always, use the right tool for the right job!

Programming
Swift
Mobile
iOS
Software Development
Recommended from ReadMedium