为什么要用锁?
为了保证多线程访问一块公共资源时,对资源的保护。或者说是多线程安全 or 线程同步
但是线程同步的实现并不是只有加锁才能解决,串行队列也是一种解决方式。
锁通用使用步骤
//带❀的是一定要有的步骤。
❀初始化锁 | 赋予一定参数
❀加锁 | 通过一定条件加锁
等待 | 线程进入 wait 等待条件
❀处理公共资源代码 { }
❀解锁 | 给锁赋予条件
销毁锁 & 锁的属性
❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀正片
❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀
1.OSSpinLock (Deprecated
)
介绍: 是一种'自旋锁'
使用:
#import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
//尝试加锁
BOOL lockStatus = OSSpinLockTry(&lock);
//你需要保护的操作
{}
//解锁
OSSpinLockUnlock(&lock);
#define OS_SPINLOCK_INIT 0 (就是把lock赋值为 0)
不过这个锁已经被废弃掉了。可查看.h文件中的介绍。
'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
废弃原因:
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
but, 除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
2.os_unfair_lock
介绍: 是一种低级锁('Low-level'),'互斥锁' ,看了好多博客说是自旋锁,其实都是错的。
os_unfair_lock虽然是 'OSSpinLock' 的替代品,但是它确实是互斥锁。
👇有对os_unfair_lock是互斥锁的考证。
.h中的官方解释
Does not spin on contention but waits in the kernel to be woken up
by an unlock
使用方法
#import <os/lock.h>
//静态初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
bool isCanLock = os_unfair_lock_trylock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
3.pthread_mutex_t
介绍: 是一种跨平台的锁(Linux,Unix,OS,iOS),本质上是一种 互斥锁,可以动态初始化。
根据传入的参数生成对应的锁.(e.g. 递归锁)
使用介绍
#import <pthread.h>
//静态初始化锁
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
//动态初始化
pthread_mutex_t mutex;
//初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//传入 PTHREAD_MUTEX_RECURSIVE (递归锁属性。)
//PTHREAD_MUTEX_ERRORCHECK(错误检查)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
pthread_mutex_init(&mutex, NULL); (初始化属性可为null)
//注: # define NULL ((void*)0)
//动态初始化锁
pthread_mutex_init(&mutex, &attr);
//销毁,一定销毁对应的属性。
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
//加锁解锁
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
// 关于另一种属性的解释 PTHREAD_MUTEX_ERRORCHECK
This type of mutex provides error checking. A thread attempting to relock
this mutex without first unlocking it shall return with an error. A thread
attempting to unlock a mutex which another thread has locked shall return
with an error. A thread attempting to unlock an unlocked mutex shall
return with an error.
4.pthread_cond_t
介绍:条件锁,是pthread_mutex_t引申出来的锁。
配合pthread_mutex_t来一起使用,可以用于线程的同步。亦或者是解决线程间的依赖关系。
当当前线程进入 wait 之后, 当前线程 mutex 会放开,保证其他线程可以拿到锁 mutex 执行,
直到收到 signal 信号或者broadcast之后才会唤醒 当前线程,并且 唤醒后再次对 mutex 进行加锁。
//条件锁
pthread_cond_t cond;
//静态初始化
pthread_cond_t cond2 = PTHREAD_COND_INITIALIZER;
pthread_condattr_t condAttr;
//初始化attr参数
pthread_condattr_init(&condAttr);
//动态初始化,也可不传attr参数
pthread_cond_init(&cond, &condAttr);
pthread_cond_init(&cond, NULL);
//1.放开当前锁 2.使当前线程进入休眠(wait) 3.唤醒后会再次mutex程加锁
pthread_cond_wait(&cond, &mutex);
//在time之前等待,之后放开锁。
pthread_cond_timedwait(&cond, &mutex, const struct timespec *restrict _Nullable);
//唤醒一个被wait的线程
pthread_cond_signal(&cond);
//唤醒所有被wait的线程
pthread_cond_broadcast(&cond);
//销毁attr 和cond
pthread_condattr_destroy(&condAttr);
pthread_cond_destroy(&cond);
5.pthread_rwlock_t
介绍: 读写锁,(互斥锁的进化)分为读锁(rlock)和写锁(wlock),可以有多个线程共同持有读锁,但是写锁只能有一个线程持有,如果读锁被持有是,写锁是不能持有的。
需要等待读锁unlock 才能持有写锁,同样需要写锁unlock才能持有读锁。
具体使用
//静态初始化
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
_rwlock = lock;
//动态初始化
pthread_rwlockattr_init(&_rwlock_attr);
pthread_rwlock_init(&_rwlock, &_rwlock_attr);
- (void)__add {
//写锁上锁
pthread_rwlock_wrlock(&_rwlock);
[super __add];
pthread_rwlock_unlock(&_rwlock);
}
- (void)__readArr {
//读锁上锁
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"self.lockArr=%@",self.lockArray);
pthread_rwlock_unlock(&_rwlock);
}
- (void)dealloc {
//销毁 锁 & 锁的属性
pthread_rwlockattr_destroy(&_rwlock_attr);
pthread_rwlock_destroy(&_rwlock);
}
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
6.NSLock 、NSCondition 、NSConditionLock和NSRecursiveLock
简介: 都属于互斥锁。
NSLock 底层是对 pthread_mutex_t 的封装.对应的参数是 PTHREAD_MUTEX_NORMAL
NSCondition 底层则是对 pthread_cond_t 的封装.
NSConditionLock 的底层则是使 NSCondition 实现的.
NSRecursiveLock 则是对 pthread_mutex_t 的 PTHREAD_MUTEX_RECURSIVE 参数的封装。
实现原理可以通过 GNUstep 查看
以上都是苹果对pthread_mutex的封装,让锁的使用更面向对象了。
具体使用
NSLock *lock = [[NSLock alloc] init];
//尝试加锁
BOOL isLocked = [lock tryLock];
[lock lock];
[lock unlock];
//由于 NSCondition 是对 pthread_cond_t 的封装,所以使用方法与 pthread_cond_t 基本一致。
//不同的是不需要我们去手动销毁锁。
NSCondition *conLock = [[NSCondition alloc] init];
[conLock lock];
[conLock wait];
[conLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[conLock unlock];
[conLock signal];
[conLock broadcast];
//NSConditionLock 设置condition 保证多线程中的同步,按自己想要的顺序执行。
//先add 然后 remove。
self.conditionLock = [[NSConditionLock alloc] init]; //默认condition 是0。
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
- (void)demoTest {
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
sleep(3);
[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}
- (void)__remove {
[self.conditionLock lockWhenCondition:2];
[super __remove];
[self.conditionLock unlock];
}
- (void)__add {
[self.conditionLock lockWhenCondition:1];
[super __add];
[self.conditionLock unlockWithCondition:2];
}
// NSRecursiveLock 用法类似于 NSLock 但是可以递归加锁。
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
[recursiveLock lock];
[recursiveLock unlock];
7.dispatch_semaphore
简单来说并不是锁,而是通过信号的方式,可以实现锁的一种机制。
简单使用
//create 的value 代表最多有几个信号量
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_after(dispatch_time( DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"走到了块里");
dispatch_semaphore_signal(sema);//发送1个信号量
});
NSLog(@"等待-----");
//如果信号量的值 >0,就让信号量的值减1,然后继续往下执行代码
//如果信号量的值 <= 0,就让线程 `sleep` (休眠).直到信号量 >0.
dispatch_wait(sema, DISPATCH_TIME_FOREVER);
// dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); //两个方法都可
NSLog(@"完成”);
2018-07-13 16:34:17.524572 AddressBook[6830:473890] 等待-----
2018-07-13 16:34:20.823293 AddressBook[6830:473911] 走到了块里
2018-07-13 16:34:20.823515 AddressBook[6830:473890] 完成
8.@synchronized(id obj) { }
简介: 互斥锁
关于 更深的synchronized的实现。实际上也是对 pthread_mutex 的递归锁的一个封装。
简单使用和底层实现:
@synchronized(id obj) {
//公共资源操作
NSLog(@"加锁");
}
实现原理: 调用堆栈
0x107d25111 <+2193>: callq 0x107d27b68 ; symbol stub for: objc_sync_enter
0x107d25139 <+2233>: callq 0x107d27ab4 ; symbol stub for: NSLog
0x107d2514a <+2250>: callq 0x107d27b6e ; symbol stub for: objc_sync_exit
0x107d2515c <+2268>: callq 0x107d27b44 ; symbol stub for: objc_release
通过查看 objc4-723 中 objc-sync.mm 源码,可以知道:
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;
}
SyncData结构体如下
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
objc_sync_enter 中 通过 synchronized 传入的对象obj 生成 data 结构体指针
然后data 在 LIST_FOR_OBJ(obj) (static StripedMap<SyncList> sDataLists)中
取出对应的 mutex.lock.
这个obj 就作为这个锁的key,从对应的hash表中找到对应的锁。
只要传入的obj相同,对应的锁就相同。
如果传入nil 则 // @synchronized(nil) does nothing (什么也做)
mutex 对应的就是 recursive_mutex_t。
通过源码再往里面查看就知道 synchronized 实质就是 一把 RECURSIVE 的pthread_mutex_t (递归锁)。
lockdebug_recursive_mutex_lock(recursive_mutex_t *lock)
{
auto& locks = ownedLocks();
setLock(locks, lock, RECURSIVE);
}
补充 atomic (原子性) 很好的参考博客
改变setter,getter方法的实现,对方法进行加锁和解锁的操作(原子性操作)。
保证 setter和getter方法内部线程同步。底层实现是 os_unfair_lock 。
但是: 并不能保证 使用atomic修饰的属性 的线程安全。
而且性能消耗太大, 因为 setter和getter 方法调用频率太高!!
源码实现:
objc4-723 中全局搜索atomic 发现在 objc-abi.h 文件中的
objc_setProperty(id _Nullable self, SEL _Nonnull _cmd, ptrdiff_t offset,
id _Nullable newValue, BOOL atomic, signed char shouldCopy)
方法中。通过调用栈查看具体实现:
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
} //省略部分代码
由 reallySetProperty 方法可知,如果是atomic 则会在 set 前生成 PropertyLocks 锁。
set 值之后 解锁
对应的 getter 方法中 的实现
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();
//再通过源码查看
StripedMap<spinlock_t> PropertyLocks;
slotlock是PropertyLocks通过 slot 从StripedMap 获取。
在查看 slotlock定义
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
}
可见 底层是通过 os_unfair_lock 实现。
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
几种主要锁的类别
互斥锁 sleep
是一种 low-level 的锁,相对于自旋锁来说比较低级。如果发现没有持有锁,则使线程进入sleep 状态。
自旋锁 busy-wait
相当于是一个外部死循环。当其他线程访问被锁的资源后,会一直进行循环,进入 busy-wait的状态,自旋锁不会引起调用者休眠,节省了线程休眠的状态切换,所以有更高的效率。
直到其他线程锁放开,因为线程一直在进行执行,所以会一直占用cpu资源。
递归锁
可以让当前线程递归的去给当前线程加锁,然后解锁。
比如:
//动态初始化
pthread_mutex_t mutex;
//初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
- (void)pthread_mutex_recursive {
//加锁
pthread_mutex_lock(&mutex);
for (int i=0; i<5; i++) {
//递归调用
[self pthread_mutex_recursive];
}
pthread_mutex_unlock(&mutex);
}
注意点
使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
1.建立锁所需要的资源
2.当线程被阻塞时所需要的资源
同步方案的性能排序(待考证) 从高到低
- os_unfair_lock
- OSSpinLock (Deprecated)
- dispatch_semaphore
- pthread_mutex_t
- dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL); (本文未做详细介绍)
- NSLock
- NSCondition
- pthread_mutex( recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
为什么 @synchronized 执行效率比 NSLock效率低?
锁的粒度: @synchronized 的锁粒度更粗,它锁定的是一个对象,而不是一个具体的代码块。这意味着如果多个临界区使用了相同的对象作为锁,那么它们将会互斥,即使它们没有直接的竞争条件。相比之下,NSLock 允许更细粒度的控制,可以只锁定需要保护的临界区,从而减少了互斥的范围,提高了并发性能。
互斥锁的开销: @synchronized 在内部使用了互斥锁来实现线程同步,而 NSLock 也使用了类似的机制。然而,由于 @synchronized 是一个语言级别的特性,其实现可能比 NSLock 更复杂,并且可能会引入一些额外的开销,如锁的创建和释放等。
底层实现差异: @synchronized 的底层实现可能会依赖于运行时系统的特性,而 NSLock 则是直接调用系统提供的互斥锁机制。因此,NSLock 的实现可能更接近操作系统的底层机制,性能上可能会更高效一些。
关于 os_unfair_lock 是互斥锁的考证
thread_9中对资源加锁,在thread_10中对os_unfair_lock_lock()的实现进行disassembly 查看。
下面是调用栈,省略了其他步骤
-> 0x10d7628c4 <+20>: movq 0x43bd(%rip), %rdi ; OSUnfairLock._unfair_lock
-> 0x10d762fb6 <+0>: jmpq *0x217c(%rip); os_unfair_lock_lock
-> 0x1128d334b <+19>: jmp 0x1128d3350; _os_unfair_lock_lock_slow
-> 0x1128d33cd <+125>: callq 0x1128d3ae6 ; _os_ulock_wait
-> 0x1128d3afa <+20>: callq 0x1128d5318 ; symbol stub for: __ulock_wait
-> 0x1128d5318 <+0>: jmpq *0x1d5a(%rip); __ulock_wait
-> 0x1128ae31c <+8>: syscall
当调用玩 syscall的时候线程进入休眠而不是进行自旋。所以 os_unfair_lock是互斥锁
#0 0x00000001128ae31e in __ulock_wait ()
用到的资源
写在最后: 关于技术的运用,总结一句话:知识决定你的下限,但是想象力决定你的上限。熟练的运用在项目中才是我们最需要的。
如果本文帮助了你,也可以赞助我一哈,O(∩_∩)O哈哈~