Asynchronous Programming on iOS
January 15, 2020
Asynchronous Programming is a way of writing software where there is a main control flow (called the main thread which usually deals with the interface with the user) and it is possible to do computation or work outside this main control flow such that when the computation is done the main control flow is notified. This is somwhat related but not exactly the same as the idea of Concurrent Programming which is a more general idea and simply means the ability to do multiple different multiple units of computation or work at the same time.
This post is an attempt to explain in depth asynchronous (and to some extent concurrent programming on multi-core devices) on iOS and but in order to do lets first briefly look at what iOS is.
iOS - A Brief Summary
iOS is a mobile operating system that was developed and released by Apple in 2007 for the first version of the iPhone. The kernel of iOS is the XNU Kernel of Darwin that is also used in MacOS. Darwin is a Unix-like operating system that was first released by apple in 2000 and is POSIX-Compliant like other popular operating systems such as GNU Linux or Solaris.
POSIX Threads (pthread)
Due to this POSIX-Compliancy iOS can therefore reuse the models of execution and libraries that are used in Linux and other POSIX-Compliant Operating Systems. POSIX Threads which is otherwise known as pthread is a model of execution on POSIX systems that exists indecently of a language. Each flow of work is known as a thread and the creation and management of these threads is done by calls to the POSIX Threads API for which implementations are available on each POSIX Compliant OS.
There are about a 100 POSIX thread procedures, all prefixed with pthread_ and deal with the four main groups of tasks
- Thread Management - Creating, Joining threads etc.
- Mutexes - To enforce mutual exclusion concurrency control of resources.
- Condition Variables - Variables that make threads wait for a condition to happen.
- Synchronization - Between threads using locks and barriers.
While an in depth discussion of these topics is out of scope for this post, we can certainly look at simple example of creating an asynchronous control flow using the POSIX Threads API. The example below demonstrates a simple way to create a number of asynchronous flows of execution and then waiting for them to complete before exiting the program. The creation is done with the pthread_create call and the waiting (also called joining) with the pthread_join call.
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 2
void *perform_work(void *arguments){
int index = *((int *)arguments);
printf("THREAD %d: Started.\n", index);
sleep(2);
printf("THREAD %d: Ended.\n", index);
}
int main(void) {
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
int i;
int result_code;
//create all threads one by one
for (i = 0; i < NUM_THREADS; i++) {
printf("IN MAIN: Creating thread %d.\n", i);
thread_args[i] = i;
result_code = pthread_create(&threads[i], NULL, perform_work, &thread_args[i]);
assert(!result_code);
}
printf("IN MAIN: All threads are created.\n");
//wait for each thread to complete
for (i = 0; i < NUM_THREADS; i++) {
result_code = pthread_join(threads[i], NULL);
printf("IN MAIN: Thread %d has ended.\n", i);
}
printf("MAIN program has ended.\n");
return 0;
}
NSThread, NSTimer and NSRunloop
In addition to POSIX compliant APIs, iOS and other XNU Darwin based operating systems by Apple had their main development environment in a language called Objective-C. Objective-C is a an object-oriented programming language that adds smalltalk-style message passing features to C. Since it is a superset of C, all C apis and libraries (including pthread) are also available in it.
To make the tasks easier for developers Objective C APIs called NSThread and NSTimer were also included even in the first version of iOS. NSThread has a similar interface and execution model as pthread and thus has similar APIs including mutexes, locks etc. As the name implies NSTimer is a special kind of threading which deals with tasks that have to run repeatedly after specific intervals of time.
Lets now look at an example to achieve the same results using NSThread with Objective-C similar to the previous C example.
#import <Foundation/Foundation.h>
#define NUM_THREADS 2
@interface MyThread : NSThread
@property NSInteger argument;
@end
@implementation MyThread
- (void)main
{
@autoreleasepool {
// The main thread method goes here
printf("THREAD %ld: Started.\n", (long)self.argument);
sleep(2);
printf("THREAD %ld: Ended.\n", (long)self.argument);
}
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray* threads = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < NUM_THREADS; i++) {
printf("IN MAIN: Creating thread %ld.\n", (long)i);
MyThread *thread = [[MyThread alloc] init];
thread.argument = i;
[threads addObject:thread];
[thread start];
}
printf("IN MAIN: All threads are created.\n");
//wait for each thread to complete
for (MyThread* thread in threads) {
while (!thread.finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}
printf("IN MAIN: Thread %ld has ended.\n", (long)thread.argument);
}
printf("MAIN program has ended.\n");
}
return 0;
}
We see here some added abstractions like autoreleasepool and NSRunLoop. While a discussion of autoreleasepool is outside the scope of this text, let’s dive a little deeper into an NSRunLoop.
A run loop is an abstraction that (among other things) provides a mechanism to handle system input sources (sockets, ports, files, keyboard, mouse, timers, etc). Each NSThread has its own run loop, which can be accessed via the currentRunLoop method. In general, you do not need to access the run loop directly, though there are some (networking) components that may allow you to specify which run loop they will use for I/O processing. A run loop for a given thread will wait until one or more of its input sources has some data or event, then fire the appropriate input handlers to process each input source that is “ready.”. After doing so, it will then return to its loop, processing input from various sources, and sleeping if there is no work to do.
In our example above instead of just blocking till the thread completes, we are actually asking the run loop in the main thread to process other events that the system might have received. This is slightly better although probably a premature optimization for our example. However in input heavy or network heavy applications this can boost performance.
NSOperationQueue
As we see in our previous example, the abstractions are quite leaky and its easy for programmers to make mistakes and write applications that end up blocking or creating a lot of unused threads. In practice programmers usually end up making another abstraction called a thread pool that limits the number of threads and introduces even more complexity. Therefore to make tasks simple Apple introduced another API called NSOperationQueue to improve concurrent programming on iOS.
NSOperation represents a single unit of work. It is an abstract class that offers a useful, thread-safe structure for modeling state, priority, dependencies, and management. Simply wrapping computation into an object doesn’t do much without a little oversight. That’s where NSOperationQueue comes in. NSOperationQueue regulates the concurrent execution of operations. It acts as a priority queue, such that operations are executed in a roughly first-in-first-out manner, with higher-priority ones getting to jump ahead of lower-priority ones. It can also limit the maximum number of concurrent operations to be executed at any given moment.
Note that we are not talking in terms of threads anymore. We are talking about just operations or tasks that we want to perform concurrently and let the underlying APIs create the necessary threads or even have a pool of threads. This actually makes it possible to run the same concurrent code on multi-core architectures and somewhat removes the necessity of using locks for mutual exclusion since we can now create a queue with just one operation at a time (called a serial queue).
Let us now try to rewrite our example in Objective C using NSOperationQueues.
#import <Foundation/Foundation.h>
#define NUM_OPERATIONS 2
@interface MyOperation : NSOperation
@property NSInteger argument;
@end
@implementation MyOperation
- (void)main
{
@autoreleasepool {
// The main thread method goes here
printf("OPERATION %ld: Started.\n", (long)self.argument);
sleep(2);
printf("OPERATION %ld: Ended.\n", (long)self.argument);
}
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
for (NSInteger i = 0; i < NUM_OPERATIONS; i++) {
MyOperation *operation = [[MyOperation alloc] init];
operation.argument = i;
[queue addOperation:operation];
}
printf("IN MAIN: All operations are created.\n");
[queue waitUntilAllOperationsAreFinished];
printf("MAIN program has ended.\n");
}
return 0;
}
As you can see instead of trying to join threads we just wait for the operation queue to finish all tasks.
Grand Central Dispatch
Now if you were to set a breakpoint inside the main function of the MyOperation class you would see that a lot interesting things are going on behind the hood.
We do see that there are a total of three threads and pthread apis are being used. But there is also a series of calls that are prefixed with dispatch. These calls are a part of library called Grand Central Dispatch or libdispatch that was introduced by Apple in 2009. It has been ported to other POSIX sytems like FreeBSD and Linux.
GCD basically takes the idea of queues and implements it on a kernel level on using threads and another non POSIX compliant kernel API called kernel work-queues or kqueue that Apple introduced in API version 10.9. This means that the actual work queue thread pool management is handled by the kernel and is not even in user mode. In fact when GCD was introduced, NSOperationQueue was re-architectured and reimplemented with GCD.
Lets now try to write the same example using GCD and this time we will use Swift which is a new language that Apple introduced in 2014 to simplify programming on iOS. Please note that the previous two examples can also be written in Swift because it has interoperability with Objective-C and C. It is now the official language for programming on iOS. The same code can also be written in Objective C of course.
import Foundation
let group = DispatchGroup()
let queue = DispatchQueue.global()
let numThreads = 2
for i in 0...numThreads {
queue.async(group: group) {
print("OPERATION \(i): Started.");
sleep(2);
print("OPERATION \(i): Ended.");
}
}
group.wait()
print("IN MAIN: All operations are created.");
As you can probably notice now we are at a very simple non leaky abstraction in a simpler language too. If you were to put a breakpoint inside the closure on the sleep call you will see that we are doing similar threading like operation queue but now we dont have any Objective-C calls anymore so we are slightly more efficient. Under the hood we are now using a kernel level method called dispatch_group_async from libdispatch do our asynchronous programming.
With that we conclude our discussion of asynchronous programming on iOS. A more astute reader can look into the references for more details or to gain a deeper understanding.
References
Written by Sumeru Chatterjee who lives in Cali, Colombia and Amsterdam, Netherlands. Follow him on Twitter.