一、同步概要
The presence of multiple threads in an application opens up potential issues regarding safe access to resources from multiple threads of execution. Two threads modifying the same resource might interfere with each other in unintended ways.
二、同步工具
2.1 Atomic Operations 线程安全修饰符
Atomic operations are a simple form of synchronization that work on simple data types. The advantage of atomic operations is that they do not block competing threads. For simple operations, such as incrementing a counter variable, this can lead to much better performance than taking a lock.
atomic
是最简单的同步方式适用于简单类型的数据对象,与lock不同的是它是不阻塞相互竞争的线程的。所以它具备更好的性能。
For a list of supported atomic operations, see the
/usr/include/libkern/OSAtomic.hheader file or see the
atomicman page.
具体的实现机制可以看/usr/include/libkern/OSAtomic.h
头文件或者使用man
命令。
2.2 Memory Barriers and Volatile Variables(内存栅栏与Volatile变量)
In order to achieve optimal performance, compilers often reorder assembly-level instructions to keep the instruction pipeline for the processor as full as possible. As part of this optimization, the compiler may reorder instructions that access main memory when it thinks doing so would not generate incorrect data. Unfortunately, it is not always possible for the compiler to detect all memory-dependent operations. If seemingly separate variables actually influence each other, the compiler optimizations could update those variables in the wrong order, generating potentially incorrect results.
编译器在编译的时候会对汇编指令重新排序,来尽可能的和处理器指令流水线保持一致,来提高应用性能。甚至会调整主内存的访问顺序(编译器不能完全保障相关的内存问题),这将会导致这些变量以错误的顺序更新,产生错误。
2.2.1 memory barrier 内存栅栏
A memory barrier is a type of nonblocking synchronization tool used to ensure that memory operations occur in the correct order.A memory barrier acts like a fence, forcing the processor to complete any load and store operations positioned in front of the barrier before it is allowed to perform load and store operations positioned after the barrier. Memory barriers are typically used to ensure that memory operations by one thread (but visible to another) always occur in an expected order.
内存栅栏是一种非阻塞同步工具,用于确保内存操作以正确的顺序进行。
内存栅栏就像一个栅栏一样强制处理器完成栅栏前所有的加载
和存储
工作后执行栅栏后相关的加载
和存储
函数操作。
使用OSMemoryBarrier函数即可使用内存栅栏功能。
2.2.1 Volatile variables
Volatile variables apply another type of memory constraint to individual variables. The compiler often optimizes code by loading the values for variables into registers. For local variables, this is usually not a problem. If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used. You might declare a variable as volatile if its value could be changed at any time by an external source that the compiler may not be able to detect 。
编译器通常会把全局变量优化到一个注册器中(局部变量不存在这样的问题),这可能会导致要用到这个全局变量的线程无法获取这个变量的变化状态。Volatile 修饰符可以强制编译器将这个变量放入内存中而不是注册器中。
Because both memory barriers and volatile variables decrease the number of optimizations the compiler can perform, they should be used sparingly and only where needed to ensure correctness.
因为这两种方式一定程度上阻止了编译器进相关的优化,所以尽可能的在必要的地方使用。
2.3 locks 线程锁
Locks are one of the most commonly used synchronization tools. You can use locks to protect a critical section of your code, which is a segment of code that only one thread at a time is allowed access.
线程锁是最常用的一种同步工具。你可以用它来保护一段可能出现竞争危险的代码块,来确保同一时间只有一个线程可以访问它。
线程锁类型
类型 | 描述 |
---|---|
Mutex | 相关的资源只允许一个线程访问,在当前的线程未释放对它的访问权时,其他想访问该资源的线程将会被阻塞直到当前线程释放 |
Recursive lock | 递归锁是互斥锁的变体。 递归锁定允许单个线程在释放之前多次获取锁定。 其他线程会一直处于阻塞状态,直到锁的所有者释放该锁的次数与获取它的次数相同。 递归锁主要在递归迭代期间使用,但也可能在多个方法需要分别获取锁的情况下使用。 |
Read-write lock | 读写锁也被称为共享排他锁。 这种类型的锁通常用于较大规模的操作(频繁的数据读取和偶尔的数据修改),可以获得很好的性能。 在正常操作期间,多个线程可以同时读取共享数据。 然而,当一个线程想要写入结构时,它会阻塞,直到所有读者释放锁,在此时它才会获取锁来修改数据。 当这个写入线程正在等待锁时,新的读取器线程将阻塞,直到写入线程完成。 系统仅支持使用POSIX线程的Read-write lock。 |
Distributed lock | 分布式锁在进程级提供互斥访问。 与真正的互斥锁不同,分布式锁不会阻止进程或阻止进程运行。 它只是报告锁何时忙,并让进程决定如何继续。 |
Spin lock | 自旋锁反复轮询其锁定条件,直到该条件成立。 自旋锁最常用于预计等待锁定时间较短的多处理器系统。 在这些情况下,轮询通常比阻塞线程更有效,后者涉及上下文切换和线程数据结构的更新。 由于轮询性质,系统不提供自旋锁的任何实现,但是您可以在特定情况下轻松实现它们。 有关在内核中实现自旋锁的信息,请参阅内核编程指南。 |
Double-checked lock | 双重检查锁试图通过在锁定之前测试锁定标准来降低获取锁的开销。 由于双重检查的锁可能不安全,系统不提供对它们的明确支持,因此不鼓励使用它们。 |
Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.
通常线程锁会配合内存栅栏一起使用
2.4Conditions
The difference between a condition and a mutex lock is that multiple threads may be permitted access to the condition at the same time. The condition is more of a gatekeeper that lets different threads through the gate depending on some specified criteria.
One way you might use a condition is to manage a pool of pending events. The event queue would use a condition variable to signal waiting threads when there were events in the queue. If one event arrives, the queue would signal the condition appropriately. If a thread were already waiting, it would be woken up whereupon it would pull the event from the queue and process it. If two events came in to the queue at roughly the same time, the queue would signal the condition twice to wake up two threads.
Conditions相比互斥锁更像是一个gatekeeper ,多个线程可以同时访问Conditions,但是由Conditions特定的规则来决定哪个线程可以运行。
2.5 Perform Selector Routines
Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received.
2.6 Synchronization Costs and Performance
同步将会带来一定性能上的损耗,这里官网指出了一个列表,测试环境是在不考虑竞争的情况,光创建互锁和atomic引起的时间消耗。
跟别说互斥的情况下(会让线程等待),所消耗的性能时间了。所以在开发多线程的时候需谨慎设置所需。
三、Tips for Thread-Safe Designs (线程安全指南)
Synchronization tools are a useful way to make your code thread-safe, but they are not a panacea. Used too much, locks and other types of synchronization primitives can actually decrease your application’s threaded performance compared to its non-threaded performance.
错误的同步设置,将会使多线程应用的性能不如单线程应用。
3.1 Avoid Synchronization Altogether (完全避免同步)
The best way to implement concurrency is to reduce the interactions and inter-dependencies between your concurrent tasks. If each task operates on its own private data set, it does not need to protect that data using locks. Even in situations where two tasks do share a common data set, you can look at ways of partitioning that set or providing each task with its own copy. Of course, copying data sets has its costs too, so you have to weigh those costs against the costs of synchronization before making your decision.
最好的方式是尽量减少并发任务之间的交互。
1.即便是两个任务享有一段共有的的数据,也可以通过分开数据或者copy这段数据分别给两个任务。
理解具体同步工具的特性
3.2 Be Aware of Threats to Code Correctness 预知代码正确性带来的风险
When using locks and memory barriers, you should always give careful thought to their placement in your code. Even locks that seem well placed can actually lull you into a false sense of security.
注意lock和栅栏的位置,它可能会使你产生错误的安全感。
Memory management and other aspects of your design may also be affected by the presence of multiple threads, so you have to think about those problems up front. In addition, you should always assume that the compiler will do the worst possible thing when it comes to safety. This kind of awareness and vigilance should help you avoid potential problems and ensure that your code behaves correctly.
3.3 Watch Out for Deadlocks and Livelocks注意死锁和活锁
Any time a thread tries to take more than one lock at the same time, there is a potential for a deadlock to occur. A deadlock occurs when two different threads hold a lock that the other one needs and then try to acquire the lock held by the other thread. The result is that each thread blocks permanently because it can never acquire the other lock.
死锁是由于两个线程皆拥有锁导致两个线程都block的现象。
A livelock is similar to a deadlock and occurs when two threads compete for the same set of resources. In a livelock situation, a thread gives up its first lock in an attempt to acquire its second lock. Once it acquires the second lock, it goes back and tries to acquire the first lock again. It locks up because it spends all its time releasing one lock and trying to acquire the other lock rather than doing any real work.
活锁
避免方法:
The best way to avoid both deadlock and livelock situations is to take only one lock at a time. If you must acquire more than one lock at a time, you should make sure that other threads do not try to do something similar.
3.4 Use Volatile Variables Correctly
If the mutex alone is enough to protect the variable, omit the volatile keyword.
It is also important that you do not use volatile variables in an attempt to avoid the use of mutexes. In general, mutexes and other synchronization mechanisms are a better way to protect the integrity of your data structures than volatile variables. The
volatilekeyword only ensures that a variable is loaded from memory rather than stored in a register. It does not ensure that the variable is accessed correctly by your code.
四、线程同步实践
4.1 Using Atomic Operations
Nonblocking synchronization is a way to perform some types of operations and avoid the expense of locks. Although locks are an effective way to synchronize two threads, acquiring a lock is a relatively expensive operation, even in the uncontested case. By contrast, many atomic operations take a fraction of the time to complete and can be just as effective as a lock.
不阻塞线程是一种避免使用lock这种消耗新能的同步方式。
These operations rely on special hardware instructions (and an optional memory barrier) to ensure that the given operation completes before the affected memory is accessed again. In the multithreaded case, you should always use the atomic operations that incorporate a memory barrier to ensure that the memory is synchronized correctly between threads.
Atomic这些操作依赖于特殊的硬件指令,以确保在受影响的内存再次访问之前完成给定的操作。 在多线程的情况下,您应始终使用包含内存屏障的原子操作来确保内存在线程之间正确同步。
4.2 Using the NSLock Class
In addition to the standard locking behavior, the
NSLockclass adds the tryLock and lockBeforeDate: methods. The tryLock method attempts to acquire the lock but does not block if the lock is unavailable; instead, the method simply returns
NO. The lockBeforeDate: method attempts to acquire the lock but unblocks the thread (and returns
NO) if the lock is not acquired within the specified time limit.
4.3 Using the @synchronized Directive
This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes。
4.4 Using an NSRecursiveLock Object
The NSRecursiveLock class defines a lock that can be acquired multiple times by the same thread without causing the thread to deadlock. A recursive lock keeps track of how many times it was successfully acquired. Each successful acquisition of the lock must be balanced by a corresponding call to unlock the lock. Only when all of the lock and unlock calls are balanced is the lock actually released so that other threads can acquire it.
递归锁,加锁要和解锁的次数一样。
4.5 Using an NSConditionLock Object
Typically, you use an NSConditionLock object when threads need to perform tasks in a specific order, such as when one thread produces data that another consumes.
4.6 Using an NSDistributedLock Object
The NSDistributedLock class can be used by multiple applications on multiple hosts to restrict access to some shared resource, such as a file.
4.7 Using Conditions
Conditions are a special type of lock that you can use to synchronize the order in which operations must proceed. They differ from mutex locks in a subtle way. A thread waiting on a condition remains blocked until that condition is signaled explicitly by another thread.
Using the NSCondition Class
The NSCondition class provides the same semantics as POSIX conditions, but wraps both the required lock and condition data structures in a single object. The result is an object that you can lock like a mutex and then wait on like a condition.