[iOS开发]iOS中的相关锁
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS开发]iOS中的相关锁相关的知识,希望对你有一定的参考价值。
锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。
注:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。
在ios中锁的基本种类只有两种:互斥锁、自旋锁,其他的比如条件锁、递归锁、信号量都是上层的封装和实现。
一、NSSpinLock
iOS10之后被弃用,使用os_unfair_lock
替代。
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
OSSpinLock
就是典型的自旋锁。自旋锁的特点是在没有获取到锁时,锁已经被添加,还没有被解开时,OSSpinLock
处于忙等状态,一直占用CPU,类似如下伪代码:
while(锁没解开);
优先级反转:
由于线程调度,每条线程的分配时间权重不一样,当权重小的线程先进入OSSpinLock
优先加锁,当权重大的线程再来访问,就阻塞在这,可能权重大的线程会一直分配到cpu所以一直会进来,但是因为有锁,只能等待,权重小的线程得不到cpu资源分配,所以不会解锁,造成一定程度的死锁。
// 初始化
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
OSSpinLockTry(&spinLock)
/*
OSSpinLock已经不再线程安全,OSSpinLock有潜在的优先级反转问题
*/
(一)atomic
自旋锁的实际应用,自动生成的setter
方法会根据修饰符不同调用不同方法,最后统一调用reallySetProperty
方法,其中就有一段关于atomic
修饰词的代码。
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
加锁处理 - 非原子性的属性除了没加锁,其他逻辑与
atomic
一般无二
前面刚说OSSpinLock
因为安全问题被废弃了,但苹果源码怎么还在使用呢?其实点进去就会发现用os_unfair_lock
替代了OSSpinLock
(iOS10之后替换)。
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t
os_unfair_lock mLock;
...
getter
方法也是如此:atomic
修饰的属性进行加锁处理。
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);
atomic
修饰的属性绝对安全吗?
atomic
只能保证setter
、getter
方法的线程安全,并不能保证数据安全
(二)读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的CPU数
写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的
如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。
// 导入头文件
#import <pthread.h>
//普通初始化
// 全局声明读写锁
pthread_rwlock_t lock;
// 初始化读写锁
pthread_rwlock_init(&lock, NULL);
//宏定义初始化
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
// 读操作-加锁
pthread_rwlock_rdlock(&lock);
// 读操作-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写操作-加锁
pthread_rwlock_wrlock(&lock);
// 写操作-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 释放锁
pthread_rwlock_destroy(&lock);
二、互斥锁
pthread_mutex
就是互斥锁——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠。是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区,从而达成效果。
这里有两个要注意的点:互斥跟同步:
- 互斥就是当多个线程进行同一操作的时候,同一时间只有一个线程可以进行操作。
- 同步是多个线程进行同一操作的时候,按照相应的顺序执行。
互斥锁又分为两种情况,可递归和不可递归。
互斥锁(Mutual exclusion,缩写Mutex)是防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。互斥锁又分为:
- 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用(比如
@synchronized
、NSRecursiveLock
) - 非递归锁:不可重入,必须等锁释放后才能再次获取锁
os_unfair_lock
、pthread_mutex
是典型的互斥锁,在没有获取到锁时锁已经被添加;还没有被解开时,它们都会让当前线程进入休眠状态,即不占用CPU资源。但是为什么,互斥锁比自旋锁的效率低呢,是因为休眠,以及唤醒休眠,比忙等更加消耗CPU资源。
NSLock
封装的pthread_mutex
的PTHREAD_MUTEX_NORMAL
模式
(一)NSLock(互斥锁、对象锁)
NSLock
是对互斥锁的简单封装,使用如下:
// 初始化
NSLock *_lock = [[NSLock alloc]init];
// 加锁
[_lock lock];
// 解锁
[_lock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_lock tryLock];
- (void)test
self.testArray = [NSMutableArray array];
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++)
dispatch_async(dispatch_get_global_queue(0, 0), ^
[lock lock];
self.testArray = [NSMutableArray array];
[lock unlock];
);
NSLock
在AFNetworking
的AFURLSessionManager.m
中有使用到。Swift对Foundation开源了,可以下载到源码,用来探究NSLock的底层实现:
open class NSLock: NSObject, NSLocking
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init()
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
deinit
#if os(Windows)
// SRWLocks do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
#endif
mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
open func lock()
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
open func unlock()
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
open func `try`() -> Bool
#if os(Windows)
return TryAcquireSRWLockExclusive(mutex) != 0
#else
return pthread_mutex_trylock(mutex) == 0
#endif
open func lock(before limit: Date) -> Bool
#if os(Windows)
if TryAcquireSRWLockExclusive(mutex) != 0
return true
#else
if pthread_mutex_trylock(mutex) == 0
return true
#endif
#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
guard var endTime = timeSpecFrom(date: limit) else
return false
#if os(WASI)
return true
#else
return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
#endif
open var name: String?
从源码来看就是对互斥锁的封装。
使用互斥锁NSLock异步并发调用block块,block块内部递归调用自己,问打印什么?
- (void)test
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
static void (^block)(int);
block = ^(int value)
NSLog(@"加锁前");
[lock lock];
NSLog(@"加锁后");
if (value > 0)
NSLog(@"value——%d", value);
block(value - 1);
[lock unlock];
NSLog(@"解锁后");
;
block(10);
);
输出:
原因:互斥锁在递归调用时会造成堵塞,并非死锁——这里的问题是后面的代码无法执行下去。
- 第一次加完锁之后还没出锁就进行递归调用
- 第二次加锁就堵塞了线程(因为不会查询缓存)
解决方案: 使用递归锁NSRecursiveLock
替换NSLock
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
(二)pthread_mutex(互斥锁)
pthread_mutex
就是互斥锁本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠。
#import <pthread/pthread.h>
// 初始化(两种)
// 1.普通初始化
pthread_mutex_t mutex_t;
pthread_mutex_init(&mutex_t, NULL);
// 2.宏初始化
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
// 加锁
pthread_mutex_lock(&mutex_t);
// 这里做需要线程安全操作
// 解锁
pthread_mutex_unlock(&mutex_t);
// 尝试加锁,可以加锁时返回的是 0,否则返回一个错误
pthread_mutex_trylock(&mutex_t)
// 释放锁
pthread_mutex_destroy(&mutex_t);
(三)@synchronized
@synchronized可能是日常开发中用的比较多的一种互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低。
// 初始化
@synchronized(锁对象)
/*
底层封装的pthread_mutex的PTHREAD_MUTEX_RECURSIVE 模式,
锁对象来表示是否为同一把锁
*/
接下来就通过源码探索来看一下@synchronized。
- (void)run
@synchronized (self)
NSLog(@"s1");
将上面源代码转化成cpp代码:
static void _I_MyPerson_run(MyPerson * self, SEL _cmd)
id _rethrow = 0; id _sync_obj = (id)self;
objc_sync_enter(_sync_obj);
try
struct _SYNC_EXIT
_SYNC_EXIT(id arg) : sync_exit(arg)
~_SYNC_EXIT() objc_sync_exit(sync_exit);
id sync_exit;
_sync_exit(_sync_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rx_h53wjns9787gpxxz8tg94y6r0000gn_T_MyPerson_9b8773_mi_3);
catch (id e) _rethrow = e;
struct _FIN _FIN(id reth) : rethrow(reth)
~_FIN() if (rethrow) objc_exception_throw(rethrow);
id rethrow;
_fin_force_rethow(_rethrow);
synchronized
调用了try catch
,内部调用了objc_sync_enter
和objc_sync_exit
。
1. objc_sync_enter
// 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;
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
- 首先从它的注释中
recursive mutex
可以得出@synchronized
是递归锁 - 如果加锁的对象
obj
不存在时分别会走objc_sync_nil()
和不做任何操作。这也是@synchronized
作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁。 - 正常情况下(obj存在)会通过id2data方法生成一个SyncData对象
typedef struct alignas(CacheLineSize) SyncData struct SyncData* nextData; DisguisedPtr<objc_object> object; int32_t threadCount; // number of THREADS using this block recursive_mutex_t mutex; SyncData;
- nextData指的是链表中下一个SyncData
- object指的是当前加锁的对象
- threadCount表示使用该对象进行加锁的线程数
- mutex即对象所关联的锁
2. objc_sync_exit
// 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;
3. 注意事项
- 不能使用非OC对象作为加锁条件——
id2data
中接收参数为id类型 - 多次锁同一个对象会有什么后果吗——会从高速缓存中拿到data,所以只会锁一次对象
- 都说@synchronized性能低——是因为在底层增删改查消耗了大量性能
- 加锁对象不能为nil,否则加锁无效,不能保证线程安全
(四)os_unfair_lock
由于OSSpinLock
自旋锁的bug,替代方案是内部封装了os_unfair_lock
,而os_unfair_lock
在加锁时会处于休眠状态,而不是自旋锁的忙等状态 os_unfair_lock
属于互斥锁。
#import <os/lock.h>
// 初始化
os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&unfair_lock);
// 解锁
os_unfair_lock_unlock(&unfair_lock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
os_unfair_lock_trylock(&unfair_lock);
/*
注:解决不同优先级的线程申以上是关于[iOS开发]iOS中的相关锁的主要内容,如果未能解决你的问题,请参考以下文章