看了这么多的源码,相信大家对锁已经见得很多了。在iOS中有8大锁,他们的性能如下:
下面我们将会分析锁的底层原理,看看锁的性能为什么有好有坏?
在这之前,我们需要先了解一些名词:
TLS线程相关解释
线程局部存储(Thread Local Stirage,TLS
):是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux
系统下,通常是通过pthread
库中的
pthread_key_create()
pthread_getspecific()
pthread_setspecific()
pthread_key_delete()
互斥锁
在Posix Thread
中定义了有一套专门用于线程同步的mutex
函数。
mutex用于保证在任何时刻,都只有一个线程访问该对象。当获取锁操作失败的时候,线程会进入睡眠,等待锁释放时被唤醒。
1. 创建和销毁
A. Posix
定义了一个宏PTHREAD_MUTEX_INITIALIZER
来静态初始化互斥锁
B. int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
C. pthread_mutex_destroy()
用于注销一个互斥锁
2. 互斥锁属性
3. 锁操作
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
语义和int pthread_mutex_lock(pthread_mutex_t *mutex)
类似,不同的是在锁已经被占据时返回busy
而不是挂起等待
互斥锁分为递归锁和非递归锁。
@synchronized
递归互斥锁
-
可重入 嵌套
准备一分简单的测试代码
- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self addMethod];
});
}
}
- (void)addMethod {
@synchronized (self) {
self.number = [NSNumber numberWithInteger:(self.number.integerValue + 1)];
NSLog(@"%@",self.number);
}
}
由于加锁的原因,nunber
的增长没有受到多线程的影响
- 在
@synchronized
出打上断点,然后bt
堆栈
并没有看出什么,接着我们打开Debug -->Debug Workflow —> Always Show Disassembly
显示汇编,发现了一个没有见过的方法objc_sync_enter
。
继续往下翻,还可以找到一个退出的方法objc_sync_exit
我们有理由怀疑这两个就是加锁和解锁的实际方法。也可以用
clang
编译,可以更直观的看到这两个方法。下面是我用clang
编译出来的代码。
下面我们下一个符号断点,看看这两个方法在哪个源码中?
- 打开
libobjc
源码,搜索objc_sync_enter
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 alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
- 查看
recursive_mutex_t
using recursive_mutex_t = recursive_mutex_tt;
这是一个递归锁的
lock
和unlock
方法
- 查看
id2data
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //在本地线程空间中查找
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
//缓存中的data与传入的匹配
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;//通过COUNT_KEY获取lockCount
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");//没有线程,而你从线程中找到了,直接报错
}
switch(why) {
case ACQUIRE: {
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);//在缓存中查找
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
//在全局缓存中找到意味着开辟了新的线程 threadCount ++
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// 第一次进来,没有找到
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {//把数据设置到线程缓存中
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// 把数据设置到缓存中
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
- 搜索
objc_sync_exit
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;
}
总结:
- 第一次进来的时候,当前线程空间没有缓存,全局的线程缓存也不会有,
首次赋值将threadCount、lockCount设置为1
,保存在tls和cache中
。 - 下次同一个线程进来,在
tls
中找到,lockCount++
tls
重新保存。 - 下次不同线程进来,在
cache
中找到,lockCount++
、threadCount++
,tls
和cache
重新保存
正因为@synchronized
需要在各种线程缓存中查找、设置、保存才导致性能不佳。
优点是方便简单,不用解锁
注意
使用@synchronized
要注意的问题
- 加锁的对象不能为
nil
- (void)viewDidLoad {
[super viewDidLoad];
_mArr = [NSMutableArray array];
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self addMethod];
});
}
}
- (void)addMethod {
@synchronized (_mArr) {
_mArr = [NSMutableArray array];
}
}
上面例子运行会出现崩溃。
打开
Xcode
中僵尸模式,更能说明问题。
再次运行
对一个已经释放了的对象发送
release
方法,才会崩溃。
这是因为
_mArr
一直在创建,新值retain
,旧值release
,_mArr
在某一个时刻会置为nil
,而@synchronized
锁nil
的时候会直接return
,没有效果。一般我们都是锁self
,他和_mArr
生命周期一样,而且不会置为nil
;当他置为nil
的时候,_mArr
也没有意义了。
- 当然
self
也不宜锁过多,如果所有的锁都锁self
,底层缓存的链表里就到处都是self
,不便查找也浪费性能。
将上述的锁换为NSLock
,一样可以达到效果。
- (void)viewDidLoad {
[super viewDidLoad];
_mArr = [NSMutableArray array];
[self addMethod];
}
- (void)addMethod {
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
_mArr = [NSMutableArray array];
[lock unlock];
});
}
}
运行不会报错。
NSLock
NSLock使用的非常多,也很简单。
- 点击
- (void)lock
方法,进入NSLocking
协议
往下翻我们可以看到NSLock、NSConditionLock、NSRecursiveLock和NSCondition
都遵循NSLocking
协议
-
通过符号断点查看
NSLock
的源码归属
NSLock
源码在Foundation
库中,我们大家都知道Foundation
没有开源,这里我们可以借助swfit
开源代码。 -
搜索
NSLock:
找到源码
我们可以看到init
方法做了很多操作,我们使用的时候一定要调用init
方法 -
向下查找
lock
方法
直接调用pthread_mutex_lock
互斥锁 -
向下查找
unlock
方法
调用pthread_mutex_unlock
解锁,然后让所有等待的线程wakeup
。
NSLock
只是简单的封装pthread
,消耗了一点性能,所以它的性能略低于pthread
NSLock的弊端
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
上面的代码运行会出现问题,程序会一直等待。原因是NSLock
只是一个普通的互斥锁,递归加锁会在还没有解锁的情况下重复加锁,造成死锁。
解决方案:
-
将
NSLock
锁移至递归的外面
这种方法的堵塞情况可能会很差,它把所有的方法都锁住了。 -
换一个递归互斥锁
NSRecursiveLock
查看swfit
的Foundation
源码
-
搜索
NSRecursiveLock
-
查找
lock
-
查找
unlock
这两段代码和NSLock
一模一样,那么为什么NSRecursiveLock
是一个递归锁呢?
问题出在init
方法中
将
NSRecursiveLock
标识为递归锁。
NSRecursiveLock的弊端
- 使用比较复杂,稍微写错位置可能就会崩溃。不如
@synchronized
简便
总结:
- 如果只是简单的加锁,优先使用
NSLock
,性能最好 - 嵌套加锁推荐使用
@synchronized
,如果你技术比较熟练使用NSRecursiveLock
没有问题,那么它比@synchronized
的性能更好。
NSCondition
NSCondition
的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被堵塞。
-
[condition lock];
//一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock
外等待,直到unlock
才可访问 -
[condition unlock];
//与lock成对使用 -
[condition wait];
//让当前线程处于等待状态 -
[condition signal];
//CPU发信号告诉线程不用再等待,可以继续执行
例子
- (void)ly_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ly_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ly_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ly_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self ly_producer];
});
}
}
- (void)ly_producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal];
[_testCondition unlock];
}
- (void)ly_consumer{
// 线程安全
[_testCondition lock];
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
// 保证正常流程
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
NSConditionLock
-
NSConditionLock
是锁,一旦一个线程获得锁,其他线程一定要等待。 -
[xxxx lock];
表示xxxx期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition
)那么它可以执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者是无条件锁),则等待,直至其他线程解锁。 -
[xxxx lockWhenCondition:A条件];
表示如果没有其他线程获得该锁但是锁的内部condition
不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition
等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他线程都将等待它的代码完成,直至它解锁。 -
[xxxx unlockWhenCondition:A条件];
表示释放锁,同时把内部的condition
设置为A条件 -
return = [xxxx lockWhenCondition:A条件 beforeDate:A时间];
表示如果被锁定(没获得锁),并且超过该时间则不再阻塞线程。但是注意,返回的值是NO
,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。 - 所谓的
condition
就是整数,内部通过整数比较条件
例子
- (void)lg_testConditonLock{
// 信号量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
}
这次我们换一种方式查看NSConditionLock
的原理,我们直接查看汇编代码。
在下面断点处,打开汇编。
-
添加符号断点
-
进入汇编
往下翻断点在
callq
方法(call 一般是调用方法
)register read
查看寄存器内容
可以看到调用了
lockWhenCondition:beforeDate:
方法
-
添加符号断点
-[NSConditionLock lockWhenCondition:beforeDate:]
-
进入
lockWhenCondition:beforeDate
我们可以看到调用了lock
方法,之后有调用了waitUntilDate:
NSConditionLock
源码
- 搜索
NSConditionLock
- 向下查看
lock()
方法
实际调用的是lock(before limit: Date)
lock(before limit: Date)
open func lock(before limit: Date) -> Bool {
_cond.lock()//[condition lock];
while _thread != nil {
if !_cond.wait(until: limit) {//当时间到达之后,不在等待了,直接unlock
_cond.unlock() //然而这里传入的时间是遥远的未来
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()//获取自身线程
#endif
_cond.unlock()//[condition unlock];
return true
}
- 搜索
unlock
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()//广播
_cond.unlock()////[condition unlock];
}
- 搜索
lock(whenCondition
实际执行的是lock(whenCondition: before:
- 搜索
lock(whenCondition:
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()//[condition lock];
while _thread != nil || _value != condition {//条件不匹配的时候,一直等待
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
- 搜索
unlock(withCondition
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_value = condition//把_value设置为condition
_cond.broadcast()//发出广播
_cond.unlock()//
}
broadcast()
open func broadcast() {
#if os(Windows)
WakeAllConditionVariable(cond)
#else
pthread_cond_broadcast(cond) // wait signal
#endif
}
唤醒所有等待的线程。
总结
- 线程1 调用
[conditionLock lockWhenCondition:]
,此时因为不满足当前条件,所以会进入waiting
状态,当前进入waiting
状态会释放当前的互斥锁。 - 线程3 此时调用
[conditionLock lock];
实际上是调用[conditionLock lockBeforeDate:]
,这里不需要条件对比,所以3会打印 - 接下来是线程2 执行
[conditionLock lockWhenCondition:]
,因为满足条件所以线程2会打印,打印完成后调用[conditionLock unlockWithCondition:1];
,将value
设置为1,并发送broadcast
,此时线程1接到信号,条件也符合,线程1才会打印。 -
[NSConditionLock lockWhenCondition:];
这里会根据传入的condition
与value
进行对比,如果不相等就会阻塞;如果相等才会执行。 -
[NSConditionLock unlockWithCondition:];
这里会先改变value
的值,然后进行广播,唤醒其他线程。
锁的归类
自旋锁
线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短的时间场合是有效的。
互斥锁
是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。通过将代码切片成一个个的临界区而达成
这里属于互斥锁的有:
NSLock
pthread_mutex
@synchronized
条件锁
就是条件变量,当进程的某些资源要求不满足时,就进入休眠,也就是锁住了。当资源被分配到,满足条件时,条件锁打开,进程继续执行。
NSCondition
NSConditionLock
递归锁
就是同一个线程可以加锁N次,而不会引发死锁。
NSRecursiveLock
pthread_mutex(recursive)
信号量(semaphore)
是一种更高级的同步机制,互斥锁可以说是semaphore
在取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不是单单的线程间互斥。
-dispatch_semaphore
其实基本的锁就包括三类:自旋锁、互斥锁、读写锁
其他的比如条件锁、递归锁、信号量都是上层的封装。
读写锁
读写锁实际上是一种特殊的自旋锁
,它把对公共资源的访问者分为读者和写者,读者只对共享资源进行读访问,写者才需要对共享资源进行写操作。这种锁相对于自旋锁,可以提高并发性。
因为在多处理器系统中,它允许有多个读者来访问共享资源
,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。