UIKit和Foundation的线程安全
在UIKit和Foundation中,有很多操作都是非线程安全的,比如多线程修改可变容器的内容可能会引发问题,必须在主线程设置UIImageView的image属性,苹果在Thread-Safe Class Design一文提到
It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance; it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread.
大意是只在主线程使用UIKit将会使并发控制变得简单,并且让UIKit变得线程安全将会带来很多性能开销,因此,苹果有意识不让UIKit线程安全,并让我们在主线程对UIKit进行调用。
锁
锁按照等抢锁失败时处理方式的不同分为自旋锁和互斥锁。
自旋锁(spinlock)
自旋锁只要没有锁上,就不断重试,可以理解成线程在while循环做抢锁操作。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。
while (抢锁(lock) == 没抢到) {
}
互斥锁(mutex)
互斥锁也叫互斥量,它在抢锁失败时,线程会进入休眠状态,优点就是节省CPU资源,缺点就是休眠唤醒会消耗一点时间。
互斥锁相对自旋锁,更适合于耗时的任务,比如文件IO,大量循环操作等;也适合于临界区竞争激烈的情况,线程很难抢到锁。互斥锁可以避免while循环长期暂用CPU资源,而自旋锁抢锁响应更快,更适合于抢锁等待时间短的轻量级任务,可以避免休眠与唤醒的性能消耗和延迟。
锁按照在同一线程中是否可以对某个锁重入,分为递归锁和非递归锁。
递归锁(recursive mutex)
递归锁也叫可重入锁,允许被同一线程多次进行重复加锁,而不会引起死锁。递归锁相对非递归锁增加了性能开销,尽量避免使用。
锁按照多个处于抢锁等待的线程是否需要排队分为公平锁和非公平锁。
公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 。
优:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。
缺:整体吞吐效率相对非公平锁要低,等待队列中除一个线程以外的所有线程都会阻塞,CPU唤醒线程的开销比非公平锁要大。
非公平锁(Nonfair)
加锁时不考虑排队等待问题,哪个线程先抢到先加锁。
优:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。
缺:在高并发环境下可能造成线程优先级反转和饿死现象。
读写锁(多读单写策略)
对数据或文件的多个线程同时读操作不需要加锁,因为读操作不涉及到数据源的修改,不会引发线程安全问题。
读操作和写操作之间需要加锁。举个例子,数据源是一张苹果图片,A线程将其改成一张香蕉图片,在A线程写入图片操作执行一半时候,B线程从数据源读取图片数据,则此时读到的图片资源是无效的,既不是苹果也不是香蕉。
写与写操作之间同理需要加锁。
优先级反转(Priority Inversion)
优先级反转是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所先占,使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务。
如下图,在步骤3中,TaskA由于其所需要的某个资源被低优先级的任务TaskC占用了,所以高优先级TaskA就被阻塞了。若此时加入中优先级的TaskB,由于比TaskC的优先级高,则TaskB优先执行,导致最终TaskA等待时间更长。
优先级反转的解决方式有优先级天花板和优先级继承。
优先级天花板(priority ceiling)
优先级天花板是当任务申请某资源时, 把该任务的优先级提升到可访问这个资源的所有任务中的最高优先级, 这个优先级称为该资源的优先级天花板。这种方法简单易行, 不必进行复杂的判断, 不管任务是否阻塞了高优先级任务的运行, 只要任务访问共享资源都会提升任务的优先级。
优先级继承(priority inheritance)
优先级继承是当TaskA 申请共享资源时, 如果共享资源正在被TaskC 使用,通过比较TaskC 与自身的优先级,如发现任务C 的优先级小于自身的优先级, 则将TaskC的优先级提升到自身的优先级, 任务C 释放资源 后,再恢复TaskC 的原优先级。这种方法只在占有资源的低优先级任务阻塞了高优先级任务时才动态的改变任务的优先级,如果过程较复杂, 则需要进行判断。
iOS中锁方案:
OSSpinLock
自旋锁。需要导入头文件<libkern/OSAtomic.h>
,其中libkern
是苹果XNU
内核中提供的一个C++
内核库,libkern
指 kernel library(内核库)。(源码地址:https://opensource.apple.com/source/xnu/xnu-7195.81.3/libkern/libkern/OSAtomic.h.auto.html)
OSSpinLock
会引发优先级反转问题(和前面的优先级反转例子略有不同)。(https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/?utm_source=tuicool)
高优先级因为自旋操作处于忙等状态从而占用大量 CPU,此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。
最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
相对自旋锁的线程处于忙等状态占用大量 CPU,互斥锁中高优先级的线程会进入休眠状态,避免了高优先级线程一直抢占着CPU资源。
OSSpinLock
提供OSSpinLockTry
尝试加锁函数,即加锁失败不会忙等,直接返回false。
os_unfair_lock
os_unfair_lock
从字面理解是个非公平锁,iOS10开始支持。与OSSpinLock
不同的是,os_unfair_lock
尝试获取的线程也会进入休眠,解锁时由内核唤醒。使用时需要导入头文件<os/lock.h>
。(源码地址:https://opensource.apple.com/source/libplatform/libplatform-254.80.2/include/os/lock.h.auto.html)
通过实时反汇编分析,os_unfair_lock
默认是互斥锁,但从源码看出os_unfair_lock
也可以通过方法os_unfair_lock_lock_with_options_inline
设置OS_UNFAIR_LOCK_ADAPTIVE_SPIN
可选参数,将其设置为自旋锁。
os_unfair_recursive_lock
os_unfair_recursive_lock
是非公平递归锁,iOS12开始支持,但目前是私有,不对开发者公开。(源码地址:https://opensource.apple.com/source/libplatform/libplatform-254.80.2/private/os/lock_private.h.auto.html)
pthread系列(POSIX Threads)
pthread是POSIX的线程标准,定义了创建和操纵线程的一套API。pthread支持跨平台,在unix,linux,windows,ios等系统下都支持。
注:POSIX指可移植操作系统接口(Portable Operating System Interface),是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称。
源码地址:https://opensource.apple.com/source/libpthread/
pthread_mutex_t
pthread_mutex_t
是互斥锁,可以在初始化时指定为递归锁,普通锁(非递归)或错误检查锁。
错误检查锁比普通锁效率稍微慢点,文档关于错误检查锁的说明,如果同一个线程请求同一个锁,则返回EDEADLK
。
If the mutex type is PTHREAD_MUTEX_ERRORCHECK, then error checking shall be provided. If a thread attempts to relock a mutex that it has already locked, an error shall be returned.
pthread_cond_t
pthread_cond_t
指条件变量,需要配合pthread_mutex_t
使用。
pthread_cond_wait
会对mutex
解锁,同时线程陷入陷入休眠,等待被激活(signal),若被激活同时会自动对mutex
加锁。pthread_cond_signal
会激活cond
,但最多只会激活一个线程。若要对所有休眠cond
进行激活,可使用pthread_cond_broadcast
函数。
pthread_cond_wait
必须放在pthread_mutex_lock
和pthread_mutex_unlock
之间,pthread_cond_signal
即可以放在pthread_mutex_lock
和pthread_mutex_unlock
之间,也可以放在pthread_mutex_lock
和pthread_mutex_unlock
之后。
pthread_cond_t
常用于实现多个线程临界区部分代码存在的的执行依赖的关系(A线程代码执行一半需要进入休眠,等待B线程任务完成,A线程再执行剩下代码),在比如可以解决生产者消费者问题的 生产者在缓冲区满时等待消费,消费者在缓冲区空时等待生产。
NSLock,NSRecursiveLock
NSLock
,NSRecursiveLock
是Foudation
框架中分别对pthread
的普通互斥锁和递归锁的OC面向对象封装。使用更加方便,不需要写销毁代码,但因为封装性能略有下降。
从GNUStep
中可以看出NSLock
内部实现使用pthread_mutex_t
的错误检查锁。NSRecursiveLock内部使用pthread_mutex_t
的递归锁。
/* Use an error-checking lock. This is marginally slower, but lets us throw exceptions when incorrect locking occurs.*/
NSCondition
NSCondition
是对pthread_mutex_t
和pthread_cond_t
的合并封装,使用更加简便。
NSCondition
常用于实现多个线程临界区部分代码存在的的执行依赖的关系(A线程代码执行一半需要进入休眠,等待B线程任务完成,A线程再执行剩下代码),如生产者消费者问题。
NSConditionLock(条件锁)
NSConditionLock
是对NSCondition
的进一步封装,可以设置具体的条件值。
lockWhenCondition: 1
方法调用代表当条件值为1才尝试加锁,否则处于休眠等待。
unlockWithCondition: 1
方法调用代表先将条件值设为1,然后进行解锁。
NSConditionLock
常用于实现设置多个线程之间的执行顺序,比如A线程先于B线程执行。
dispatch_queue 串行队列
dispatch_queue 串行队列
类似锁,也可以实现线程同步的效果。
dispatch_semaphore(信号量)
dispatch_semaphore_wait
函数会在信号量的值<=0时,进入休眠,等到值>0激活。当值>0时,会使信号量值-1,并继续执行后面代码。dispatch_semaphore_signal
使信号量值+1。
dispatch_semaphore
常用于实现控制线程最大并发数量。
@synchronized
@synchronized
是对os_unfair_recursive_lock
(旧版本是pthread_mutex_t
递归锁)递归锁的封装。@synchronized
的传入对象作为参数,用于内部HashMap关联锁的key。
通过反汇编调试可以看到,@synchronized
最终调用了 objc_sync_enter
和objc_sync_exit
,@synchronized
分别作为代码块的开始和结束。
可以在
Objective-C
源码中找到这两个函数。
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
通过源码追溯,@synchronized
最终使用的锁是recursive_mutex_tt对象,内部是os_unfair_recursive_lock
非公平递归锁。
class recursive_mutex_tt : nocopy_t {
os_unfair_recursive_lock mLock;
}
通过下面代码可以看到锁最终会存储在静态全局哈希表sDataLists中,key是@synchronized
作为参数传入的对象(准确说是对象所在内存地址)。
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
@synchronized
封装比较复杂,而且使用时需要在全局哈希表中查找锁,性能比较差,不推荐使用。
atomic
atomic
修饰的属性,可以保证对属性的set和get方法都是原子性操作。
从objc
源码可以看出,若设置atomic
修饰,自动生成的set方法会赋值时进行加锁slotlock,赋值结束进行解锁。在get方法中使用的和set是同一把锁(PropertyLocks[slot]),锁存放在全局哈希表PropertyLocks中。
StripedMap<spinlock_t> PropertyLocks;
/// 属性自动生成的get方法
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
/// 属性自动生成的set方法
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
atomic
使用的是spinlock_t锁,从名字可以看出是自旋锁。源码追踪到spinlock_t是mutex_tt类,内部使用os_unfair_lock非公平锁,并在加锁时对其进行设置OS_UNFAIR_LOCK_ADAPTIVE_SPIN
,将其设置为自旋锁。
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
void lock() {
lockdebug_mutex_lock(this);
// <rdar://problem/50384154>
uint32_t opts = OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION | OS_UNFAIR_LOCK_ADAPTIVE_SPIN;
os_unfair_lock_lock_with_options_inline
(&mLock, (os_unfair_lock_options_t)opts);
}
}
atomic
性能比较差,不推荐使用,有以下几个原因:
-
atomic
使用全局的哈希表存储锁,而不是直接取到锁指针,性能会有一定影响。 -
atomic
修饰的属性对所有读写场景都会加锁,但一般是该属性只有少部分场景需要加锁。 -
atomic
读写用同一把锁,可以防止读和写,写和写,读和读 同时进行,避免线程安全问题。但读与读操作却并不需要加锁,因此不是一个高效的读写锁(多读单写)。 -
atomic
有时并不能实现写与写方法的线程同步。
如下面代码,atomic
可以防止多个set方法同时调用存在的线程安全问题。比如A和B线程未设置atomic
执行set,一方面set方法内部有release和retain/copy,多线程set调用可能造成对象已经释放被再次调用release导致crash;一方面受到CPU与内存最小交互字节数影响,一般64位CPU为64位。
// A线程执行
self.i = 1;
// B线程执行
self.i = 2;
但atomic
却无法保证set方法设置的新value需要依赖旧value场景的线程安全。如下,A线程可能在执行int value = self.i + 1;
时,切换到B线程,B线程执行完再来执行A线程self.i = value;,最终导致B线程的+1变得无效。
简单来说,atomic
只在语句2 加锁,但要保证线程安全,需要对语句1 和语句2 同时加锁。
// A线程执行
self.i ++;
// B线程执行
self.i ++;
内部汇编等同于
// A线程执行
int value = self.i + 1; // 语句1
self.i = value; // 语句2
// B线程执行
int value = self.i + 1;
self.i = value;
-
atomic
也无法保证数据容器类型的线程安全,比如NSMutableArray
的addObject
方法。
pthread_rwlock高效读写锁
pthread_rwlock
提供pthread_rwlock_rdlock
和pthread_rwlock_wrlock
函数分别对读和写加锁。
gcd实现高效读写锁
使用gcd
并发队列和 barrier
可实现高效读写锁,参考https://www.jianshu.com/p/9e62aa5a5da1
性能对比
os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue串行队列 > NSLock > NSCondition > pthread_mutex递归锁 > NSRecursiveLock > NSConditionLock > @synchronized
值越大性能更好,因为dispatch_semaphore
性能比较好,很多人会将初始信号量设为1,用来当锁使用。os_unfair_lock
iOS 10才支持,若不需要支持iOS 10以下设备推荐使用。OSSpinLock
可能会导致优先级反转,不推荐使用。
可以通过以下宏定义简化dispatch_semaphore
的使用。
#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^ { \
semaphore = dispatch_semaphore_create(1); \
});\
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
-(void)test {
SemaphoreBegin
// 要加锁的代码
SemaphoreEnd
}
需要加锁场景
需要加锁场景一般分为三种:
1.多线程访问和修改同一个变量,导致变量值错误。
待更新。。。
2. 线程执行顺序错乱,导致线程同步问题。
待更新。。。
3. 死锁,活锁,饥饿。
待更新。。。