A brief description of multithreading

A thread is the smallest unit of Program execution flow. A thread consists of: unique ID, Program Counter, register set, stack. The same process can have multiple threads that share global variables and heap data for the process.

Here, PC (Program Counter) points to the current instruction address. To run our Program through the update of PC, one thread can only execute one instruction at a time. Of course, we know that threads and processes are virtual concepts. In fact, PC is a register in the CPU core, which actually exists, so it can be said that a CPU core can only execute one thread at a time.

Whether it’s a multi-processor device or a multi-core device, developers tend to only care about the number of cores in the CPU, not their physical composition. CPU core is finite, that is, a device has a finite number of concurrent threads. When the number of threads exceeds the number of CPU cores, a CPU core often has to process multiple threads, a behavior called thread scheduling.

Thread scheduling simply means that a CPU core takes turns executing each thread separately for a period of time. Of course, there is complicated logic involved, which will be discussed later.

IOS development exchange technology group: 563513413, no matter you are big bull or small white are welcome to enter, share BAT, Ali interview questions, interview experience, discuss technology, we exchange learning and growth together!

Second, multi-threaded optimization ideas

In mobile development, due to the complexity of the system, developers often cannot expect all threads to truly execute concurrently, and developers do not know when XNU switches kernel-mode threads and when thread scheduling occurs, so developers often have to consider thread scheduling.

1. Reduce thread switching

When the number of threads exceeds the number of CPU cores, the CPU core switches user-mode threads through thread scheduling, which means that there are contextual transitions (register data, stack, etc.). Excessive context switching will bring resource overhead. While kernel-mode thread switching should not theoretically be a performance burden, it should be minimized in development.

2. Thread priority tradeoff

Generally speaking, in addition to the rotation method, there is a priority scheduling scheme, in the thread scheduling, the higher priority threads will be executed earlier. Two concepts need to be clarified:

  • IO – intensive threads: threads that wait frequently and give up time slices while waiting.
  • Cpu-intensive threads: Threads that rarely wait, meaning they occupy the CPU for long periods of time.

In a special scenario, multiple CPU-intensive threads occupy all CPU resources and their priorities are high. In this case, the IO-intensive threads with lower priorities continue to wait and starve to death. Of course, in order to avoid thread starvation, the system gradually increases the priority of the “left out” thread, and IO intensive threads generally get priority increases more easily than CPU intensive threads.

The system will do this automatically, but it will always cause time to wait, which may affect the user experience. So I think developers need to weigh priorities in two ways:

  • Give PRIORITY to IO – intensive threads over CPU – intensive threads.
  • Make urgent tasks a higher priority.

For example, a large number of images are decompressed asynchronously. The decompressed images do not need to be reported to the user immediately. At the same time, a large number of asynchronous disk cache query tasks need to be reported to the user after the disk cache query tasks are completed.

Image decompression is cpu-intensive, while disk cache query is IO intensive, and the latter needs more urgent feedback to the user. Therefore, the image decompression thread should have a lower priority, while disk cache query thread should have a higher priority.

It is worth noting that this is a large number of asynchronous tasks, which means that the CPU is likely to run at full capacity, and there is no need to deal with priorities if there is more than enough CPU resources.

4. Optimization of main thread tasks

Some businesses can only be written on the main thread, such as initialization and layout of UI-class components. In fact, this aspect of the optimization is more, the industry said most of the performance optimization is to reduce the pressure of the main thread, seems to deviate from the category of multi-threaded optimization, the following is based on the main thread task management list a few points:

A memory multiplexing

Memory reuse to reduce the amount of time spent opening up memory is widely used in system UI components, such as UITableViewCell reuse. At the same time, less open memory means less free memory, which also saves CPU resources.

Lazy loading tasks

Since the UI component must be initialized on the main thread, it should be initialized on time, and swift’s write time copy is a similar idea.

Tasks are split and executed in a queue

By listening for notifications such as the end of the Runloop, a large number of tasks are broken up to perform a small number of tasks in each Runloop cycle. Before you do this, you should think about putting tasks on asynchronous threads rather than using this extreme optimization approach.

Execute tasks when the main thread is idle

// dispatch_async(dispatch_get_main_queue(), ^{// wait until the main thread is idle});

Iii. About “Lock”

Multithreading brings thread safety problems. When atomic operations cannot meet services, various “locks” are often used to ensure memory read and write security.

Commonly used locks include mutex, read-write lock, and idle lock. Generally, the mutex pthread_mutex_t and dispatch_semaphore_t in iOS development are mutex pthread_mutex_t and dispatch_semaphore_t. The read-write lock pthread_rwlock_t can meet most requirements and has good performance.

When a lock fails to be read, a thread may have two states:

  • Idle state: The thread executes an empty task loop waiting to acquire the lock as soon as it becomes available.
  • Suspended state: A thread is suspended and needs to be woken up by another thread when the lock is available.

It is time-consuming to wake up the thread, and the idle thread consumes CPU resources and the longer the time, the more the consumption. Therefore, it can be seen that the idle thread is suitable for a small number of tasks, while the suspension is suitable for a large number of tasks.

Actually lock mutex and literacy are idling lock features, they failed to get locked in when idling for a while first, then we will hang, and idling lock won’t always idle, in particular after the idling time will still hangs, so usually need not deliberately to use idling lock, Casa Taloyum there are detailed explanations in the blog.

1. OSSpinLock priority inversion problem

Priority inversion concept: for example, two threads A and B, priority A < B. When user A obtains the lock to access the shared resource, user B tries to obtain the lock. In this case, user B enters the busy state. The longer the busy state takes, the more CPU resources are occupied. As the priority of A is lower than that of B, A cannot compete with higher-priority threads for CPU resources, resulting in delayed completion of the task. Solutions to priority inversion include priority ceiling and priority inheritance, both of which have the core operation of raising the priority of threads currently accessing a shared resource.

2. Avoid deadlocks

A common scenario is that the same thread repeatedly acquires a lock resulting in a deadlock, which can be handled using a recursive lock. Pthread_mutex_t is initialized with the pthread_mutex_init_recursive() method to have a recursive lock feature.

Attempts to acquire locks such as pthread_mutex_trylock() can effectively avoid deadlocks

3. Minimize the locking task

Developers should fully understand the business and minimize the area of code contained in locks. In order to improve the performance of concurrent locks, they should not use locks to protect code that does not have thread safety problems.

4. Always be aware of the security of non-reentrant methods

When a method is reentrant, it is safe to use it. If a method is not reentrant, developers should pay attention to whether the method will be accessed by multiple threads. If so, add a thread lock.

5. Compiler overoptimization

The compiler may write a variable to the register and not write it back temporarily for the sake of efficiency, so as to facilitate the next use. We know that a code is converted into more than one instruction, so the variable may be read and written by another thread during the process of writing the variable to the register before writing it back. The compiler also reverses the order of instructions it deems irrelevant for efficiency.

The volatile keyword can prevent the compiler from caching variables into registers for efficiency without writing them back, and it can prevent the compiler from adjusting the order in which volatile modifiers are performed.

Atomic increments have a similar application: int32_t OSAtomicIncrement32(volatile int32_t *__theValue).

6. CPU execution is out of order

It is also possible for cpus to swap instruction orders for efficiency, resulting in unsafe code that is locked. This can be solved by using memory barriers, which the CPU crosses to refresh the allocation of variables in registers.

conclusion

Things that deviate from the underlying principles are more abstract. Basic technologies are often not that important in the business, but they allow you to code more calmly. Going beyond the thinking of ordinary developers also allows you to choose more reasonable and efficient solutions in more complex businesses, so that your code can be reliable.