Tidbit: Multithreading in iOS

Introduction

If you don't know what threads are, you probably don't need to look at
this guide. You should go learn what threads are, as well as the associated
perils, and then come back. If you're reading this guide, you're probably
making a HTTP request or running an O(2^n!) algorithm and don't particularly
like having your UI hang in the meantime. You're in the right place.

A Short Overview of Threads on iOS

Most lines of code that you've written in Xcode hitherto have
happened on the UI thread. This is the specific thread where all
UI responsivity exists--UIView updates, button touches, and gesture
recognition, among other things. You are designated one and only one
UI thread (and if you know more about the innards of threading, it's
basically the scheduler's favorite thread to schedule--i.e. it has super
high priority).

There is one very golden rule to UI design (according to me),
and it's to NEVER EVER EVER block the UI thread. If you do, your
app becomes unresponsive--button presses, swipes, and all animations cease
to exist--until you unblock the thread. This is considered A Bad
Thing, and might actually cause your app to be rejected from The App
Store. So, if you have to make an HTTP request (takes a second or two to
return), or
Bogosort 100,000
items (could literally take forever), do not do it on the thread which
responds to the user.

There's a second golden rule: don't update the UI on a thread which
isn't the UI thread. Think about why this may be the case--we'll
show you more of an example later.

So, we'll show you what it looks like when you make an HTTP request on the
UI thread, and then we'll show you how to mitigate this problem.

IsItCarnivalYet, revisited

Blocking the UI Thread

You might remember the
Is
It Carnival Yet app we built in a previous lecture. Well we're going to
dive into some of the base code to examine what happens when we poll
the IsItCarnivalYet.com website on the
UI thread. First, we see how the app should work:

Pretty smooth, right? We see have an indicator pop up to tell the user
that stuff is actually happening behind the scenes. Works pretty nicely.
Let's try to naively recreate this experience. Consider the following
implementation:

When we consider the sequential execution of this code, it should
theoretically recreate the above experience. Alas...

Moving Everything to Another Thread

This app sucks. We didn't even get to see the indicator start spinning.
We need to make the .carnivalYet() happen on a different thread
so that we can achieve a bit more responsiveness in this app. This is where
the beloved dispatch_* family of functions come into play. Consider
the following code:

That's some weird stuff happening there. But let's break it down. On the
UI thread, we set the answerLabel text to "CHECKING...", unhide the
indicator, and start its animation. We punted the rest of the code to a
completely different thread. More specifically, we punted the rest of
the code to QOS_CLASS_USER_INITIATED-type thread, which is something
we'll explain later. So, everything that happens in that Swift block is all
code that is run on another thread. In theory, we should see the wheel spinning.

Okay. It's literally taking 15 seconds
to return our HTTP call. While it's great that our wheel is spinning and our
button is responsive, this ends up STILL being really bad for UI responsivity
if our multithreading is actually making our app take longer than if we
didn't have multithreading. So what's the problem?

Moving Some Stuff Back to the UI Thread

Remember the second golden rule of designing UIs--changes to the UI
should happen on the UI thread. This has to do with a lot of the thread
internals in the iOS kernel--basically, it takes a lot of work to
make UI changes on a thread that isn't the UI thread. Due to some
nondeterminism in the kernel code, your UI-code-running-on-the-non-UI-thread
could take anywhere between a few extra seconds to a few extra minutes to
actually run. Or it may never run at all. Or your app may crash!

So, we punted all of the heavy work to another thread. But we have to
punt any UI changes we make back to the main thread. This is actually
really easy to do, for a job that sounds somewhat complicated. We just
make another dispatch_async call, as follows:

And now it works perfectly (taking into account the 5 seconds of
added latency).

Quality of Service Classes

Remember above when we made the dispatch_get_global_queue call
and we passed in this weird QOS_CLASS_USER_INITIATED? Well that
is a parameter which specifies how important the work being done on
that thread is. The above class specifies that work being done is user
initiated, which essentially translates to the scheduler as really
damn important. So, it will schedule work on that thread as much as it
can without compromising the UI (or other potentially more important
threads).

You can specify different qualities of service to basically signify
how important some work is. If you have 100 different jobs running on the
USER_INITIATED queue, and some of them are not really super
important, your app responsivity suffers for no good reason. If you're
doing some behind-the-scenes work, for example, that's less important and
can most certainly go on a less important queue to allow room for the
really important stuff.

In order of importance, you may use QOS_CLASS_USER_INTERACTIVE
(as important as the UI thread), QOS_CLASS_USER_INITIATED (really
important but not moreso than the UI), QOS_CLASS_DEFAULT (eh, it's
not terribly necessary--choose this if you're not sure what QOS class
to choose), QOS_CLASS_UTILITY (doing some work that
the user isn't really waiting for anytime soon), and
QOS_CLASS_BACKGROUND (the user doesn't need to know this stuff
is happening). For more information, check out the official documentation
at qos.h:

/*!
* @constant QOS_CLASS_USER_INTERACTIVE
* @abstract A QOS class which indicates work performed by this thread
* is interactive with the user.
* @discussion Such work is requested to run at high priority relative to other
* work on the system. Specifying this QOS class is a request to run with
* nearly all available system CPU and I/O bandwidth even under contention.
* This is not an energy-efficient QOS class to use for large tasks. The use of
* this QOS class should be limited to critical interaction with the user such
* as handling events on the main event loop, view drawing, animation, etc.
*
* @constant QOS_CLASS_USER_INITIATED
* @abstract A QOS class which indicates work performed by this thread
* was initiated by the user and that the user is likely waiting for the
* results.
* @discussion Such work is requested to run at a priority below critical user-
* interactive work, but relatively higher than other work on the system. This
* is not an energy-efficient QOS class to use for large tasks and the use of
* this QOS class should be limited to operations where the user is immediately
* waiting for the results.
*
* @constant QOS_CLASS_DEFAULT
* @abstract A default QOS class used by the system in cases where more specific
* QOS class information is not available.
* @discussion Such work is requested to run at a priority below critical user-
* interactive and user-initiated work, but relatively higher than utility and
* background tasks. Threads created by pthread_create() without an attribute
* specifying a QOS class will default to QOS_CLASS_DEFAULT. This QOS class
* value is not intended to be used as a work classification, it should only be
* set when propagating or restoring QOS class values provided by the system.
*
* @constant QOS_CLASS_UTILITY
* @abstract A QOS class which indicates work performed by this thread
* may or may not be initiated by the user and that the user is unlikely to be
* immediately waiting for the results.
* @discussion Such work is requested to run at a priority below critical user-
* interactive and user-initiated work, but relatively higher than low-level
* system maintenance tasks. The use of this QOS class indicates the work should
* be run in an energy and thermally-efficient manner.
*
* @constant QOS_CLASS_BACKGROUND
* @abstract A QOS class which indicates work performed by this thread was not
* initiated by the user and that the user may be unaware of the results.
* @discussion Such work is requested to run at a priority below other work.
* The use of this QOS class indicates the work should be run in the most energy
* and thermally-efficient manner.
*
* @constant QOS_CLASS_UNSPECIFIED
* @abstract A QOS class value which indicates the absence or removal of QOS
* class information.
* @discussion As an API return value, may indicate that threads or pthread
* attributes were configured with legacy API incompatible or in conflict with
* the QOS class system.
*/

A More...Modern Approach

Believe it or not, you are tapping into some really old C functions.
While this works, it really doesn't fit with Apple's object oriented style.
As of iOS 8, there are some new ways to properly do multithreading, and it's
something everyone should resolve to learn by iOS 9. For more information,
check out this blog entry.
Everything is super well explained (by a CMU alum, too!).