前言
我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁应运而生。
这里顺便提一下,上锁的两种方式trylock和lock使用场景:当前线程锁失败,也可以继续其它任务,用 trylock 合适当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock
锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。 注:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
在iOS中锁的基本种类只有两种:互斥锁、自旋锁,其他的比如条件锁、递归锁、信号量都是上层的封装和实现
以下是十种线程锁所用时间:
1 自旋锁 OSSpinLock
(iOS10之后被弃用,使用os_unfair_lock替代)
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock); os_unfair_lock是一种互斥锁
线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
OSSpinLock 就是典型的自旋锁 自旋锁的特点是在没有获取到锁时既锁已经被添加,还没有被解开时. OSSpinLock处于忙等状态,一直占用CPU资源,类似如下伪代码:
while(锁没解开);
关于优先级反转问题:
由于线程调度,每条线程的分配时间权重不一样,当权重小的线程先进入OSSpinLock优先加锁,当权重大的线程再来访问,就阻塞在这,可能权重大的线程会一直分配到cpu所以一直会进来,但是因为有锁,只能等待,权重小的线程得不到cpu资源分配,所以不会解锁,造成一定程度的死锁。
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
OSSpinLockTry(&spinLock)
/*
OSSpinLock已经不再线程安全,OSSpinLock有潜在的优先级反转问题
注:苹果爸爸已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换,
顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.
*/
1.1 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方法的线程安全,并不能保证数据安全
@property (atomic, assign) NSInteger index;
- (void)test {
self.index = 0;
// 发现这里如果用全局队列dispatch_barrier_async无效
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i ++) {
self.index = self.index + 1;
NSLog(@"%d--%ld", i, (long)self.index);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i ++) {
self.index = self.index + 1;
NSLog(@"%d--%ld", i, (long)self.index);
}
});
dispatch_barrier_async(queue, ^{
NSLog(@"%ld", (long)self.index);
});
}
被atomic修饰的index变量分别在两次并发异步for循环10000次后输出的结果并不等于20000。由此可以得出结论:
- atomic保证变量在取值和赋值时的线程安全
- 但不能保证self.index+1也是安全的
- 如果改成self.index=i是能保证setter方法的线程安全的
1.2 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的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_rwlock_t,更多的是采用其他方式,例如使用栅栏函数完成读写锁的需求
2 互斥锁
pthread_mutex就是互斥锁本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠
是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区,而达成。这里有两个要注意的点互斥跟同步,互斥就是当多个线程进行同一操作的时候,同一时间只有一个线程可以进行操作。同步是多个线程进行同一操作的时候,按照相应的顺序执行。互斥锁又分为两种情况,可递归和不可递归。
互斥锁(Mutual exclusion,缩写Mutex)防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒 互斥锁又分为:
- 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用(比如@synchronized, NSRecursiveLock)
- 非递归锁:不可重入,必须等锁释放后才能再次获取锁
os_unfair_lock 、pthread_mutex是典型的互斥锁,在没有获取到锁时既锁已经被添加,还没有被解开时.它们都会让当前线程进入休眠状态既不占用CPU资源,但是为什么,互斥锁比自旋锁的效率低呢,是因为休眠,以及唤醒休眠,比忙等更加消耗CPU资源.
NSLock 封装的pthread_mutex的PTHREAD_MUTEX_NORMAL 模式
2.1 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中有使用到 想要了解一下NSLock的底层原理,但发现其是在未开源的Foundation源码下面的,但但是Swift对Foundation却开源了,可以在swift-corelibs-foundation下载到源码来一探究竟:
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 {
NSRecursiveLock *lock = [[NSRecursiveLock 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);
});
}
输出结果并没有按代码表面的想法去走,而是只打印了一次value值
加锁前
加锁后
value——10
加锁前
原因: 互斥锁在递归调用时会造成堵塞,并非死锁——这里的问题是后面的代码无法执行下去
- 第一次加完锁之后还没出锁就进行递归调用
- 第二次加锁就堵塞了线程(因为不会查询缓存)
解决方案: 使用递归锁NSRecursiveLock替换NSLock
2.2 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);
YYKit的YYMemoryCach有使用到pthread_mutex
2.3 @synchronized
@synchronized可能是日常开发中用的比较多的一种互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低
// 初始化
@synchronized(锁对象){
}
/*
底层封装的pthread_mutex的PTHREAD_MUTEX_RECURSIVE 模式,
锁对象来表示是否为同一把锁
*/
接下来就通过源码探索来看一下@synchronized在使用中的注意事项
- 通过汇编能发现@synchronized就是实现了objc_sync_enter和 objc_sync_exit两个方法
- 通过符号断点能知道这两个方法都是在objc源码中的
- 通过clang也能得到一些信息:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
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);
}
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);
}
}
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
在objc源码中找到objc_sync_enter和objc_sync_exit
// 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;
}
1、首先从它的注释中recursive mutex可以得出@synchronized是递归锁,如果锁的对象obj 2、不存在时分别会走objc_sync_nil()和不做任何操作(源码分析可以先解决简单的逻辑分支)
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
这也是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁
3、正常情况下(obj存在)会通过id2data方法生成一个SyncData对象
- nextData指的是链表中下一个SyncData
- object指的是当前加锁的对象
- threadCount表示使用该对象进行加锁的线程数
- mutex即对象所关联的锁
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;
4、注意事项
- 不能使用非OC对象作为加锁条件——id2data中接收参数为id类型
- 多次锁同一个对象会有什么后果吗——会从高速缓存中拿到data,所以只会锁一次对象
- 都说@synchronized性能低——是因为在底层增删改查消耗了大量性能
- 加锁对象不能为nil,否则加锁无效,不能保证线程安全
- (void)test {
_testArray = [NSMutableArray array];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.testArray) {
self.testArray = [NSMutableArray array];
}
});
}
}
上面代码一运行就会崩溃,原因是因为在某一瞬间testArray释放了为nil,但哈希表中存的对象也变成了nil,导致synchronized无效化
解决方案:
- 对self进行同步锁,这个似乎太臃肿了
- 使用NSLock
2.4 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);
/*
注:解决不同优先级的线程申请锁的时候不会发生优先级反转问题.
不过相对于 OSSpinLock , os_unfair_lock性能方面减弱了许多.
*/
typedef mutex_t spinlock_t;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
public:
constexpr mutex_tt() : mLock(OS_UNFAIR_LOCK_INIT) {
lockdebug_remember_mutex(this);
}
constexpr mutex_tt(__unused const fork_unsafe_lock_t unsafe) : mLock(OS_UNFAIR_LOCK_INIT) { }
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);
}
void unlock() {
lockdebug_mutex_unlock(this);
os_unfair_lock_unlock_inline(&mLock);
}
void forceReset() {
lockdebug_mutex_unlock(this);
bzero(&mLock, sizeof(mLock));
mLock = os_unfair_lock OS_UNFAIR_LOCK_INIT;
}
void assertLocked() {
lockdebug_mutex_assert_locked(this);
}
void assertUnlocked() {
lockdebug_mutex_assert_unlocked(this);
}
// Address-ordered lock discipline for a pair of locks.
static void lockTwo(mutex_tt *lock1, mutex_tt *lock2) {
if ((uintptr_t)lock1 < (uintptr_t)lock2) {
lock1->lock();
lock2->lock();
} else {
lock2->lock();
if (lock2 != lock1) lock1->lock();
}
}
static void unlockTwo(mutex_tt *lock1, mutex_tt *lock2) {
lock1->unlock();
if (lock2 != lock1) lock2->unlock();
}
// Scoped lock and unlock
class locker : nocopy_t {
mutex_tt& lock;
public:
locker(mutex_tt& newLock)
: lock(newLock) { lock.lock(); }
~locker() { lock.unlock(); }
};
// Either scoped lock and unlock, or NOP.
class conditional_locker : nocopy_t {
mutex_tt& lock;
bool didLock;
public:
conditional_locker(mutex_tt& newLock, bool shouldLock)
: lock(newLock), didLock(shouldLock)
{
if (shouldLock) lock.lock();
}
~conditional_locker() { if (didLock) lock.unlock(); }
};
};
3 条件锁
就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。 在一定条件下,让其等待休眠,并放开锁,等接收到信号或者广播,会从新唤起线程,并重新加锁,像NSCondition封装了pthread_mutex的以上几个函数,NSConditionLock封装了NSCondition。
3.1 NSCondition
NSCondition是一个条件锁,可能平时用的不多,但与信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足
// 初始化
NSCondition *_condition= [[NSCondition alloc]init];
// 加锁
[_condition lock];
// 解锁
[_condition unlock];
/*
其他功能接口
wait 进入等待状态
waitUntilDate:让一个线程等待一定的时间
signal 唤醒一个等待的线程
broadcast 唤醒所有等待的线程
注: 所测时间波动太大, 有时候会快于 NSLock, 我取得中间值.
*/
同样的能在Swift源码中找到关于NSCondition部分
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init() {
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
}
deinit {
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
open func wait(until limit: Date) -> Bool {
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
open func signal() {
pthread_cond_signal(cond)
}
open func broadcast() {
pthread_cond_broadcast(cond) // wait signal
}
open var name: String?
}
从上述精简后的代码可以得出以下几点:
- NSCondition是对mutex和cond的一种封装(cond就是用于访问和操作特定类型数据的指针)
- wait操作会阻塞线程,使其进入休眠状态,直至超时
- signal操作是唤醒一个正在休眠等待的线程
- broadcast会唤醒所有正在等待的线程
3.2 NSConditionLock
// 初始化
NSConditionLock *_conditionLock = [[NSConditionLock alloc]init];
// 加锁
[_conditionLock lock];
// 解锁
[_conditionLock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_conditionLock tryLock];
/*
其他功能接口
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; //初始化传入条件
- (void)lockWhenCondition:(NSInteger)condition;//条件成立触发锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//尝试条件成立触发锁
- (void)unlockWithCondition:(NSInteger)condition;//条件成立解锁
- (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//触发锁 条件成立 并且在等待时间之内
*/
顾名思义,就是NSCondition + Lock 那么和NSCondition的区别在于哪里呢?接下来看一下NSConditionLock源码
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
_thread = nil
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
_thread = nil
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open var name: String?
}
从上述代码可以得出以下几点:
- NSConditionLock是NSCondition加线程数的封装
- NSConditionLock可以设置锁条件,而NSCondition只是无脑的通知信号
4 递归锁
就是同一个线程可以加锁N次而不会引发死锁。
递归锁的主要意思是,同一条线程可以加多把锁.什么意思呢,就是相同的线程访问一段代码,如果是加锁的可以继续加锁,继续往下走,不同线程来访问这段代码时,发现有锁要等待所有锁解开之后才可以继续往下走.
NSRecursiveLock 封装的pthread_mutex 的PTHREAD_MUTEX_RECURSIVE模式
4.1 NSRecursiveLock
// 初始化
NSRecursiveLock *_recursiveLock = [[NSRecursiveLock alloc]init];
// 加锁
[_recursiveLock lock];
// 解锁
[_recursiveLock unlock];
// 尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
[_recursiveLock tryLock];
/*
注: 递归锁可以被同一线程多次请求,而不会引起死锁。
即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
这主要是用在循环或递归操作中。
- (BOOL)lockBeforeDate:(NSDate *)limit;//触发锁 在等待时间之内
*/
NSRecursiveLock使用和NSLock类似,如下代码就能解决上个问题
- (void)test {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
NSRecursiveLock在YYKit中YYWebImageOperation.m中有用到
2021-12-08 19:58:53.983517+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.983709+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.983818+0800 test[64402:1714297] value——10
2021-12-08 19:58:53.983922+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.984030+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.984148+0800 test[64402:1714297] value——9
2021-12-08 19:58:53.984241+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.984349+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.984461+0800 test[64402:1714297] value——8
2021-12-08 19:58:53.984559+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.984650+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.984740+0800 test[64402:1714297] value——7
2021-12-08 19:58:53.984835+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.984919+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.985163+0800 test[64402:1714297] value——6
2021-12-08 19:58:53.985429+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.985643+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.985956+0800 test[64402:1714297] value——5
2021-12-08 19:58:53.986292+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.986608+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.986911+0800 test[64402:1714297] value——4
2021-12-08 19:58:53.987209+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.987561+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.987876+0800 test[64402:1714297] value——3
2021-12-08 19:58:53.988244+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.988616+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.989005+0800 test[64402:1714297] value——2
2021-12-08 19:58:53.989337+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.989651+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.989988+0800 test[64402:1714297] value——1
2021-12-08 19:58:53.990346+0800 test[64402:1714297] 加锁前
2021-12-08 19:58:53.990788+0800 test[64402:1714297] 加锁后
2021-12-08 19:58:53.991115+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.991446+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.991664+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.991947+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.992153+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.992422+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.992733+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.994203+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.994605+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.994919+0800 test[64402:1714297] 解锁后
2021-12-08 19:58:53.995244+0800 test[64402:1714297] 解锁后
注意事项
递归锁在使用时需要注意死锁问题——前后代码相互等待便会产生死锁 上述代码在外层加个for循环,问输出结果?
- (void)test {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
}
运行代码会崩溃,并会提示野指针错误 原因: for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁——找不到解锁的出口 即 线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁 解决: 可以采用使用缓存的@synchronized,因为它对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性
- (void)testSynchronized {
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^block)(int);
block = ^(int value) {
@synchronized (self) {
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
}
};
block(10);
});
}
}
2021-12-08 20:08:42.747650+0800 test[65536:1757963] value——10
2021-12-08 20:08:44.775675+0800 test[65536:1757963] value——9
2021-12-08 20:08:46.396463+0800 test[65536:1757963] value——8
2021-12-08 20:08:46.396638+0800 test[65536:1757963] value——7
2021-12-08 20:08:46.396825+0800 test[65536:1757963] value——6
2021-12-08 20:08:46.396997+0800 test[65536:1757963] value——5
2021-12-08 20:08:46.397161+0800 test[65536:1757963] value——4
2021-12-08 20:08:46.397280+0800 test[65536:1757963] value——3
2021-12-08 20:08:46.397420+0800 test[65536:1757963] value——2
2021-12-08 20:08:46.397549+0800 test[65536:1757963] value——1
2021-12-08 20:08:46.397700+0800 test[65536:1757962] value——10
2021-12-08 20:08:46.397839+0800 test[65536:1757962] value——9
2021-12-08 20:08:46.397971+0800 test[65536:1757962] value——8
2021-12-08 20:08:46.398456+0800 test[65536:1757962] value——7
2021-12-08 20:08:46.398808+0800 test[65536:1757962] value——6
2021-12-08 20:08:46.399218+0800 test[65536:1757962] value——5
2021-12-08 20:08:46.399655+0800 test[65536:1757962] value——4
2021-12-08 20:08:46.399880+0800 test[65536:1757962] value——3
2021-12-08 20:08:46.400112+0800 test[65536:1757962] value——2
2021-12-08 20:08:46.410242+0800 test[65536:1757962] value——1
2021-12-08 20:08:46.410377+0800 test[65536:1757960] value——10
2021-12-08 20:08:46.410471+0800 test[65536:1757960] value——9
2021-12-08 20:08:46.410599+0800 test[65536:1757960] value——8
2021-12-08 20:08:46.410695+0800 test[65536:1757960] value——7
2021-12-08 20:08:46.410805+0800 test[65536:1757960] value——6
2021-12-08 20:08:46.410908+0800 test[65536:1757960] value——5
2021-12-08 20:08:46.411018+0800 test[65536:1757960] value——4
2021-12-08 20:08:46.411134+0800 test[65536:1757960] value——3
2021-12-08 20:08:46.411232+0800 test[65536:1757960] value——2
2021-12-08 20:08:46.411334+0800 test[65536:1757960] value——1
2021-12-08 20:08:46.411466+0800 test[65536:1758278] value——10
2021-12-08 20:08:46.411652+0800 test[65536:1758278] value——9
2021-12-08 20:08:46.412014+0800 test[65536:1758278] value——8
2021-12-08 20:08:46.412314+0800 test[65536:1758278] value——7
2021-12-08 20:08:46.412644+0800 test[65536:1758278] value——6
2021-12-08 20:08:46.419722+0800 test[65536:1758278] value——5
2021-12-08 20:08:46.419858+0800 test[65536:1758278] value——4
2021-12-08 20:08:46.419966+0800 test[65536:1758278] value——3
2021-12-08 20:08:46.420071+0800 test[65536:1758278] value——2
2021-12-08 20:08:46.420186+0800 test[65536:1758278] value——1
2021-12-08 20:08:46.420319+0800 test[65536:1757965] value——10
2021-12-08 20:08:46.420436+0800 test[65536:1757965] value——9
2021-12-08 20:08:46.420527+0800 test[65536:1757965] value——8
2021-12-08 20:08:46.420635+0800 test[65536:1757965] value——7
2021-12-08 20:08:46.420728+0800 test[65536:1757965] value——6
2021-12-08 20:08:46.420933+0800 test[65536:1757965] value——5
2021-12-08 20:08:46.421128+0800 test[65536:1757965] value——4
2021-12-08 20:08:46.421321+0800 test[65536:1757965] value——3
2021-12-08 20:08:46.421515+0800 test[65536:1757965] value——2
2021-12-08 20:08:46.421710+0800 test[65536:1757965] value——1
2021-12-08 20:08:46.421910+0800 test[65536:1758443] value——10
2021-12-08 20:08:46.422080+0800 test[65536:1758443] value——9
2021-12-08 20:08:46.422292+0800 test[65536:1758443] value——8
2021-12-08 20:08:46.422541+0800 test[65536:1758443] value——7
2021-12-08 20:08:46.422926+0800 test[65536:1758443] value——6
2021-12-08 20:08:46.423221+0800 test[65536:1758443] value——5
2021-12-08 20:08:46.423520+0800 test[65536:1758443] value——4
2021-12-08 20:08:46.423776+0800 test[65536:1758443] value——3
2021-12-08 20:08:46.423981+0800 test[65536:1758443] value——2
2021-12-08 20:08:46.424317+0800 test[65536:1758443] value——1
2021-12-08 20:08:46.424655+0800 test[65536:1758442] value——10
2021-12-08 20:08:46.425001+0800 test[65536:1758442] value——9
2021-12-08 20:08:46.425188+0800 test[65536:1758442] value——8
2021-12-08 20:08:46.425462+0800 test[65536:1758442] value——7
2021-12-08 20:08:46.425694+0800 test[65536:1758442] value——6
2021-12-08 20:08:46.425950+0800 test[65536:1758442] value——5
2021-12-08 20:08:46.426378+0800 test[65536:1758442] value——4
2021-12-08 20:08:46.426598+0800 test[65536:1758442] value——3
2021-12-08 20:08:46.426892+0800 test[65536:1758442] value——2
2021-12-08 20:08:46.427324+0800 test[65536:1758442] value——1
2021-12-08 20:08:46.427731+0800 test[65536:1757964] value——10
2021-12-08 20:08:46.427963+0800 test[65536:1757964] value——9
2021-12-08 20:08:46.428261+0800 test[65536:1757964] value——8
2021-12-08 20:08:46.428580+0800 test[65536:1757964] value——7
2021-12-08 20:08:46.428769+0800 test[65536:1757964] value——6
2021-12-08 20:08:46.428998+0800 test[65536:1757964] value——5
2021-12-08 20:08:46.429226+0800 test[65536:1757964] value——4
2021-12-08 20:08:46.429447+0800 test[65536:1757964] value——3
2021-12-08 20:08:46.429709+0800 test[65536:1757964] value——2
2021-12-08 20:08:46.429999+0800 test[65536:1757964] value——1
2021-12-08 20:08:46.430415+0800 test[65536:1758598] value——10
2021-12-08 20:08:46.430628+0800 test[65536:1758598] value——9
2021-12-08 20:08:46.430830+0800 test[65536:1758598] value——8
2021-12-08 20:08:46.431080+0800 test[65536:1758598] value——7
2021-12-08 20:08:46.431332+0800 test[65536:1758598] value——6
2021-12-08 20:08:46.431656+0800 test[65536:1758598] value——5
2021-12-08 20:08:46.431855+0800 test[65536:1758598] value——4
2021-12-08 20:08:46.432200+0800 test[65536:1758598] value——3
2021-12-08 20:08:46.432415+0800 test[65536:1758598] value——2
2021-12-08 20:08:46.432765+0800 test[65536:1758598] value——1
2021-12-08 20:08:46.433096+0800 test[65536:1758279] value——10
2021-12-08 20:08:46.433311+0800 test[65536:1758279] value——9
2021-12-08 20:08:46.433562+0800 test[65536:1758279] value——8
2021-12-08 20:08:46.433911+0800 test[65536:1758279] value——7
2021-12-08 20:08:46.434324+0800 test[65536:1758279] value——6
2021-12-08 20:08:46.434658+0800 test[65536:1758279] value——5
2021-12-08 20:08:46.435084+0800 test[65536:1758279] value——4
2021-12-08 20:08:46.437160+0800 test[65536:1758279] value——3
2021-12-08 20:08:46.437598+0800 test[65536:1758279] value——2
2021-12-08 20:08:46.437986+0800 test[65536:1758279] value——1
4.2 pthread_mutex(recursive)
注: 递归锁可以被同一线程多次请求,而不会引起死锁。 即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。 这主要是用在循环或递归操作中。
#import <pthread/pthread.h>
// 初始化
pthread_mutex_t mutex_t;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&mutex_t, &attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
// 加锁
pthread_mutex_lock(&mutex_t);
// 解锁
pthread_mutex_unlock(&mutex_t);
/*
注: 递归锁可以被同一线程多次请求,而不会引起死锁。
即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。
这主要是用在循环或递归操作中。
*/
测试例子
- (void)testPthreadMutex_Recursive {
pthread_mutex_t mutex_t;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex_t, &attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^block)(int);
block = ^(int value) {
pthread_mutex_lock(&mutex_t);
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
pthread_mutex_unlock(&mutex_t);
};
block(10);
});
}
5 信号量
信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值 0/1 时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
5.1 dispatch_semaphore
// 初始化
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore_t);
/*
注: dispatch_semaphore 其他两个功能
1.还可以起到阻塞线程的作用.
2.可以实现定时器功能,这里不做过多介绍.
*/
下面这段代码要求使用信号量来按序输出(当然栅栏函数可以满足要求)
- (void)test {
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前%d----线程%@", i, [NSThread currentThread]);
});
// 使用栅栏函数
// dispatch_barrier_async(queue, ^{});
}
}
利用信号量的API来进行代码改写
- (void)test {
// 创建信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前%d----线程%@", i, [NSThread currentThread]);
// 打印任务结束后信号量解锁
dispatch_semaphore_signal(sem);
});
// 由于异步执行,打印任务会较慢,所以这里信号量加锁
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
}
输出结果
当前0----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前1----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前2----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前3----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前4----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前5----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前6----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前7----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前8----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
当前9----线程<NSThread: 0x600000c2c000>{number = 5, name = (null)}
如果当创建信号量时传入值为1又会怎么样呢?
- i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1
- i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去
当前1----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当前0----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当前2----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当前3----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当前4----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当前5----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当前6----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当前7----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
当前8----线程<NSThread: 0x600001448d40>{number = 3, name = (null)}
当前9----线程<NSThread: 0x60000140c240>{number = 6, name = (null)}
结论:
- 创建信号量时传入值为1时,可以通过两次才堵塞
- 传入值为2时,可以通过三次才堵塞
6 互斥锁和自旋锁的区别
- 互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时线程会被唤醒
- 自旋锁的线程则会一直处于等待状态(忙等待)不会进入休眠——因此效率高
总结
其实基本的锁就包括了三类,自旋锁, 互斥锁, 读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现。
- OSSpinLock不再安全,底层用os_unfair_lock替代
- atomic只能保证setter、getter时线程安全,所以更多的使用nonatomic来修饰
- 读写锁更多使用栅栏函数来实现
- @synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁
- NSLock、NSRecursiveLock、NSCondition和NSConditionLock底层都是对pthread_mutex的封装
- NSCondition和NSConditionLock是条件锁,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似
- 普通场景下涉及到线程安全,可以用NSLock
- 循环调用时用NSRecursiveLock
- 循环调用且有线程影响时,请注意死锁,如果有死锁问题请使用@synchronized
写在后面
日常开发中若需要使用线程锁来保证线程安全,请多考虑一下再选择使用哪个锁,@synchronized并不是最优的选择。作为一名优秀的开发不但能让App正常运行,更要让它优质地运行、优化它的性能