This article first introduces some basic concepts of multithreading, such as Atomicity, out-of-order execution, and Memory barrier. Then combined with the actual development of iOS, the problems of Property, Dealloc, target-Action, block, mutable containers and so on under multi-threading are analyzed. Finally, I shared a few tips.

This post is also published on my personal blog

Overview


Multithreading makes the CPU’s computing power more fully utilized, especially in the multi-core era, so the program becomes more smooth and efficient. In iOS development, multithreading can be realized at zero cost through GCD. Some time-consuming operations are often distributed to sub-threads for execution through GCD. It is because of its cheapness that we usually ignore the multithreading problems caused by it.

Multithreading is actually a double-edged sword, which brings fluency and efficiency, but also brings many problems and greatly increases the complexity of the program. In the development, it is not difficult to find that many bugs and crashes are caused by multithreading. Multithreading and its related problems generally have the following characteristics:

  • Difficult to reproduce (can reproduce the problem is not a problem), multithreading problems are generally related to a specific execution sequence, difficult to reproduce, generally difficult to find in the testing phase, and after the release of a large number of users will be exposed;
  • Difficult to understand, a lot of multithreading problems in addition to look very strange, scratching their heads, can not find other problems, not enough experience is difficult to find is caused by multithreading;
  • It’s hard to realize that when we’re writing multithreaded code, it’s hard to notice that we’re digging holes!

The focus of this article is not on multithreaded programming, but on the problems that multithreading can cause, and a few important concepts need to be introduced before continuing: atomicity, out-of-order execution, and Compiler reordering.

Atomicity


In terms of multithreading, we have to mention atomicity. We know that atom is a concept in chemistry, which refers to a fundamental particle that is not separable. In computing, atomic operations (atomicity) are operations that cannot be interrupted during a single execution. Disruption is found only between instructions, so operations that can be accomplished by a single instruction are atomic in a UniProcessor system, whereas in a Symmetrical Multi-processing (SMP) system, Single-instruction operations can also be disrupted by the presence of multiple processors running independently at the same time. Therefore, atomicity cannot be achieved by software and requires hardware level support (architecture-dependent). In short, the CPU achieves atomicity at the hardware level through cache locks and bus locks.

On most current CPU architectures (e.g. X86, PowerPC, ARM), reading and writing aligned one-word data is guaranteed to be atomic. For example, on 64-bit systems, reading and writing aligned ints is an atomic operation.

Atomicity is a very important concept in multithreading and is a prerequisite for thread synchronization (locking).

Data Races


What we usually say about multithreading is actually about Data Races in most cases. There are two conditions for the appearance of Data Races:

  • Multiple threads accessing the same block of memory without synchronization;
  • At least one of them is a write operation.

The result can be a crash, an unexpected result, or, of course, “harmless”. As mentioned above, on most current cpus, reading and writing aligned word length data are atomic operations, that is, multiple threads operate the memory simultaneously without any synchronization, which is also called “Benign Race”. But Apple engineers disagree, for two main reasons:

  • In C language standard, Benign Race belongs to undefined behavior.
  • This can cause problems on newer compilers or processors (this is not an accepted standard, after all).

Therefore, synchronization should be done for any place where data Races may appear.

Out-of-order execution, compiler reordering


Out-of-order execution and Compiler reordering are essentially the same from the perspective of code execution, because they both change the original execution order of the code. Just who’s behind it and when it happened:

  • Out-of-order execution — implemented by hardware, where the CPU changes the order of instructions as they are executed.
  • Compiler reordering – Implemented by the compiler that changes the code order during compilation (compile time).

Why is that? That’s probably what you’re more interested in right now. It’s simple, two words: “tuning” to make the processor run more efficiently. The details are beyond the scope of this article. So again, isn’t there a problem with arbitrarily changing the code order? Of course, they (CPU, Compiler) are optimized on the premise of “safe” based on “in-depth” analysis.

For example, here is the code:

For optimization purposes, setData methods might end up executing in the following order:

If Thread 2 still exists:

The problem lies in the inability of the CPU and Compiler to handle multithreading problems, which are stuck in single-threaded mode. Therefore, this pot is only to be taken by the programmer ape!

Memory barrier


Memory barrier is a solution to the problems caused by out-of-order execution and compiler reordering. Memory barriers are literal: They enforce reads and writes that precede those that follow them.

The system also provides an interface for setting Memory barriers: osMemoryBarriers.

Thus, in the example above, we simply add a Memory barrier between writes to thread1:

Memory barriers are the basis for thread synchronization through locks.

Thanks to the Memory barrier implemented inside locks, out-of-order execution, Compiler reordering, and Memory barriers are rarely considered in actual programming. But that doesn’t mean you can ignore it, and it may give you some ideas when you come across some “crazy” questions. In fact, any problem is the same, the depth of research determines the depth of thinking and breadth of vision in the analysis and solution of the problem.

With these general multithreading issues covered, let’s take a look at iOS development related issues.

Property


Before continuing, it’s worth reviewing a few concepts: variables, Pointers, and pointer variables.

  • Variables – Essentially an area allocated in memory that is typically operated on in code by a name (variable name);
  • Pointer — essentially an integer value that identifies the starting address of a block of memory;
  • Pointer variable – a variable that stores the value of a pointer.

Take a look at a simple example:

pi
int
i
i
0x0ffab1234678
pi
0x0ffab1234678
pi
0x0ffab1234660
int **

  • Value types — that is, non-pointer types, such asint,bool,double;
  • Reference type – All properties declared as pointer types (all objects in OC are of this type).

When it comes to Property thread-safety, one must think of the option to select either atomic or nonatomic attributes when defining a Property. Atomic is the default. If atomic is selected when defining Property, the system will lock the default getter and setter methods. Will atomic be as thread-safe as we want? For a value type attribute, atomic can guarantee that the operation on that attribute is Thread-safety. However, for attributes of reference type, the situation is more complicated and may not guarantee Thread-safety.

Let’s look at an example:

atomic
objs
objs
atomic
atomic
atomic

Since atomic is not as useful as it might be and has some performance issues, it is generally not used when defining attributes, but is implemented manually when synchronization is required.

There are two other points to note when using atomic attributes:

  1. If you define getter and setter methods, you need to implement atomic semantics yourself.
  2. If you access the stored variable of an attribute directly, you lose the atomic semantics.

dealloc


We know that the dealloc method will execute on the thread on which the object is finally released. The main problem here is that some classes of dealloc methods are unsafe to execute on child threads, such as UIKit Object.

When we start a new child thread, the child thread will retain its target, as in:

  • throughperformSelectorInBackground:,performSelector:onThread:Method to start a child thread;
  • throughNSThreadStart child thread;
  • The child thread is opened by GCD, and its block references target.

When a child thread retains target, it must ensure that the target is released before the main thread. Otherwise, if the child thread holds the last reference to Target, target’s dealloc method must be executed on the child thread, which is not allowed by the UI.

But interestingly, we found that starting with iOS8, even when UI objects are released on child threads, the system distributes its dealloc method to the main thread for execution.

UIViewController
dealloc
dealloc

UIViewController
dealloc
UIViewController
release

UIViewController
release

  • At 1 as shown above, throughpthread_main_npMethod to determine whether the current execution is on the main thread;
  • If it is not executed on the main thread, it directly jumps to0x117a18a1aSkip direct executiondeallocMethods);
  • 3处(0x117a18a1aDispatch to the main threaddeallocMethods.

It should be noted that so far no official documents have been found to clarify this matter. Therefore, it is best not to make such an assumption, but to make sure that the UI dealloc method is always executed on the main Thread from a code standpoint.

target-action


In Objective-C, there are two main ways to implement callbacks: target-action(observer-selector) and block. In this section, we discuss the problems with target-action mode in multi-threading. Target-action usually looks like this: (there are two objects: objA and objB)

  • objACare aboutobjBEventA;
  • objBcontainsobjAWeak /assign;
  • whenobjBEventA is called back when an event occursobjAA method of;
  • Typically an EventA event occurs on a child thread.

Because objB does not strongly reference objA, it is possible for objA to be freed in another thread when it performs a callback, resulting in wild Pointers.

  • The most common problem that observer-selector is invoked on the same thread as it is triggered is changing the UI on the child thread;
  • Observer selectors are executed synchronously.

Further analysis found that:

  • Starting with iOS9, NSNotificaionCenter strongly holds the observer for notification tossing until the observer completes its action.

    As shown in the above code, the child thread throws the notification, and after that, our code does not have a strong reference message observer, but from the result of execution, we can see that, first, there is no crash(no wild pointer), second, the observerdeallocMethods in thenotificationHandlerAfter the implementation.

    Through the observerdeallocMethod to add breakpoints can be seen in thepostNotificationName:MethoddeallocMethod execution.

  • KVO inNSKeyValueWillChangeMethod calls the observer’sretainMethod to ensure that the Observer is not released during KVO callback execution, resulting in a wild pointer problem:

Unlike KVO, NSNotificationCenter does not call the retain method of the Observer directly. It is assumed that the retain operation is performed on the Observer in a more low-level manner.

NSNotificationCenter and KVO provide us with a lot of inspiration: retain the target before the callback to prevent wild pointer problems, and release the target after the callback is completed.

However, it’s not that simple. NSNotificationCenter and KVO are not thread-safe.

KVO

As mentioned above, KVO triggers a retain Observer to prevent wild Pointers, but thread-safety issues still exist.

dealloc
removeObserver:
observeValueForKeyPath:
retain
dealloc
retain

How to solve it?

  • Do not use KVO. When I was developing the red envelope module in QQ reading project, I found such crash in gray scale, and finally gave up KVO.
  • Do not use KVO across threads;
  • Apple suggested designing the Observer as an object that would never be released, and Facebook’s famous KVO open-source frameworkFBKVOControllerThis method is used, with a singleton inside_FBKVOSharedController, designed to receive all KVO callbacks.

NSNotificationCenter

When it comes to the NSNotificationCenter, the first reaction is that it needs to be removed from the NotificationCenter in the Observer dealloc method or crash. However, starting from iOS9, there is no need to manually remove the observer in dealloc, because in the Notification Center the observer is stored as weak, and its biggest and most useful feature is that it is set to nil before the object to which it refers is released. As a result, starting with iOS9, NSNotificationCenter is thread-safe and does not have the problem of triggering callbacks during dealloc execution as KVO does.

By summing up the performance of NSNotificationCenter in iOS8 and ios9, as well as the performance of KVO, we can draw lessons to be learned when implementing target-Action (Delegate) mode:

  • Target must be weak to prevent a callback from being triggered during the Target dealloc process;
  • Retain the target before triggering the callback — to prevent wild Pointers from being released during the callback execution.

block


As one of the callback implementations, blocks are even more widely used than target-Action because they save the context. When it comes to blocks, one must think of the retain cycle problem that this causes. To solve the retain cycle problem, __block is generally used under MRC and __weak is used under ARC. Using __block under MRC is very dangerous (it is very prone to wild Pointers). You must ensure that the object to which __block points exists when the block is executed.

In ARC, there is no wild pointer due to weak, but when the block is executed, self may already be nil, and it may crash if it is not handled properly. It is better to wrap the block body as a method, in which case if self is nil, the method will not be executed.

mutable containers


Objective-c mutable containers are non-thread-safe, and the multithreading problems they cause are the most common type of thread-safe problems encountered in real development. *** was mutated while being enumerated. The most common, and most commonly caused by multithreading (one thread traverses, another thread writes). To solve this problem, two points need to be noted:

  • Must not provide external interfaces that can access mutable containers directly from within a class;
  • The read (including the whole traversal process) and write of the containers need to be synchronized by locking.

We all know that UI operations need to be performed on the main thread, except that UITableView doesn’t seem to have anything to do with multithreading, but it doesn’t. When using a UITableView, we often store its datasource in an array, either from disk or the network, almost always in a child thread.

skills


Finally, a few tips.

Thread Sanitizer (TSan)

For multithreading, Apple has introduced a scanning tool for Xcode8 (currently only available for 64-bit emulators) : Thread Sanitizer (TSan), which provides the following features:

  • Use of uninitialized mutexes;
  • Thread leaks (missing pthread_join);
  • Unsafe calls in signal Handlers (ex: Malloc);
  • Unlock from wrong thread;
  • Data RACES.

Among them, the scanning function of Data Races is very good! It is recommended that you use TSan scan for each version. For more information, see WWDC2016-412 Thread Sanitizer and Static Analysis.

Magnify the legal bit problem

Due to multithreading problems, it is generally difficult to reproduce, and can only be reproduced under a specific execution sequence, which undoubtedly increases the difficulty of investigation, analysis and problem solving. At this point, we can turn a small probability event into a large probability event through amplification method, such as repeatedly executing an operation through a cycle and increasing the unsafe window period through sleep, etc.

Provides an interface for callers to specify callback threads

There is often a question during development about which thread the callback should be executed on. The usual practice is to either dispatch to the main thread roughly, or execute directly on the current thread. Instead, it is better to provide an interface for the caller to specify which Thread callback to use. There are several open source libraries that do this, such as Facebook’s famous WebSocket library SocketRocket:

delegate
delegateDispatchQueue
delegateOperationQueue

Queue Specific Determines whether tasks are being executed in a queue

In order to execute tasks synchronously on a queue, it is often necessary to determine whether or not a deadlock exists on the queue. The dispatch_queue_get_specific and dispatch_queue_set_specific apis can be easily implemented:

summary


While multithreading brings fluency and efficiency, it also brings endless problems. The multithreading problem itself is very complex, and this paper simply analyzes several common multithreading problems. In general, there is no unified solution to the multi-threading problem, which varies from person to person, and depends more on the experience and rigorous attitude of developers.

The resources

Threading Programming Guide

Simple and Reliable Threading with NSOperation

WWDC2016-412 Thread Sanitizer and Static Analysis

Observers and Thread Safety

Out-of-order execution

Memory barrier

Why is UIViewController deallocated on the main thread?