Combine and Schedulers

How to have more control on how your program is executed with Schedulers and Combine

--

Introduction

If you are experienced with iOS app development, you surely found yourself in a thread error because you tried to update the UI from a closure or something like that, and searching on the net you discovered about DispatchQueue.main and threads.

Now, you will learn something more on them and about using them with Combine.

Let’s start saying what is a Scheduler.

From Apple documentation about Combine you can read that a Scheduler is “a protocol that defines when and how to execute a closure”, and that’s true, but it’s not all. In general, we can say that a Scheduler provides you a way to execute instructions in specific orders, providing you a way of queueing instructions.

But… why should I need to customise the way instructions are queued?

There are many reasons you may need this, for example you may want to move heavy-computing operations on a secondary queue to leave more space on the main queue to update the UI, or maybe because you want to optimise your code executing instructions queues in parallel (so trying to execute more instructions at the same time if it’s possible) or in serial (so executing one instruction at time), or because you want to make sure about the priority of your instructions,…

What about threads?

Threads can be considered the effective queue that holds instructions that will be passed to execution. They are different from schedulers: Schedulers use threads to execute instruction in the desired order. You’ll see that a Scheduler can use multiple thread!

In Combine there are 2 particular Publisher functions to analyse:

  • subscribe(on:options:) to create the subscription on a specific scheduler
  • recieve(on:options:) to emit values on a specific scheduler.

Let’s see some examples:

In these examples we’ll use DispatchQueue scheduler, that we’ll describe in detail later.

The result of the previous code is something like this:

With subscribe(on:) values are emitted from another thread (number 9 in this case) instead of the main thread. This Scheduler is a serial scheduler, as you can see by the order of the executed instructions.

If you remove “subscribe(on:)” from that code, it will be all executed on main thread:

Now, let’s see an example of recieve(on:)

receive(on:) example

With this example, you can see that using recieve(on:) the publisher emits on another thread instead of the main thread:

What do you say? It is the same as before?

Try to add “.print(Thread.current.description)” after .recieve(on: aQueue):

As you can see, values are received on the main thread, but emitted to another thread.

Schedulers

In the previous examples we used DispatchQueue scheduler, but now we’ll take a look to all schedulers that Combine framework provides more in detail:

ImmediateScheduler

Basically, we can say that the ImmedaiteScheduler schedule your instructions to make them execute immedietally on the current thread. Let’s see an example:

ImmediateScheduler example

The output will be this:

It looks like we didn’t change anything!

That’s because, as already said, this scheduler executes immediately on the current thread, and in this case that thread is the main thread.

RunLoop

RunLoop scheduler is strictly associated with threads. As Apple documentation states:

Each Thread object — including the application’s main thread — has an RunLoop object automatically created for it as needed.

Let’s see an example:

RunLoopExample

Output:

In this example we used RunLoop.current, so the RunLoop of the current thread that executed the instruction (in this case the main thread).

Working with RunLoop, however, can be dangerous, because they’re not thread safe! The best choice, if you don’t need strictly to use RunLoop, is to use DispatchQueue

DispatchQueue

As you can read from the Apple documentation:

Dispatch queues are FIFO queues to which your application can submit tasks in the form of block objects. Dispatch queues execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app’s main thread, the system makes no guarantees about which thread it uses to execute a task.

Basically, it gives you a way to schedule your instructions serially or concurrently without worrying about handling manually threads. This last part makes it the most safe way of scheduling.

Let’s see some examples:

DispatchQueueExample

Output:

Main queue result

Here we are using the main DispatchQueue (DispatchQueue.main), so all instructions are executed on main thread serially.

If you change “mainQueue” with “someSerialQueue”, you will get something like this:

Serial queue result

Here we are using another serial DispatchQueue, but it’s not the main one. We can say this by looking on which thread the publisher emits its values.

And last, but not least,try to use “someParallelQueue”:

Parallel queue result

Here we can see a parallel DispatchQueue, that executes more instructions at the same time. We can say that by seeing the wrong output order of the values that the publisher emits.

OperationQueue

From Apple documentation:

An operation queue executes its queued Operation objects based on their priority and readiness. After being added to an operation queue, an operation remains in its queue until it reports that it is finished with its task.

The particular aspect of this scheduler is that it executes operation by priority and readiness, not concerning about other aspects. So the execution can be either serial or parallel depending on operations. However, you can control how this scheduler operates.

Let’s see an example:

OperationQueueExample

Output:

Here you can see that in an OperationQueue the operations are executed in different threads without respecting an order. But we can force the OperationQueue to become serial by limiting it’s maximum operation per time.

Try to uncomment the line “opQueue.maxConcurrentOperationCount = 1” and you will have in output something like this:

Here you can see that in an OperationQueue the operations are executed in different threads without respecting an order. But we can force the OperationQueue to become serial by limiting its maximum operation per time.

In conclusion, we have seen how can we use schedulers to have more control on our code’s workflow and use these in Combine to create a solid and fluent execution of our program.

To see all the code used in this article, visit the following page:

Combine+Schedulers.playground

If you want to explore other Combine’s topics, visit our repository on GitHub:

--

--