前言
iOS中有很多锁,那么平时使用过程中到底怎么使用呢?本文分享13种加锁方案。本文较长总共一万字。文中代码在github上。
-
OSSpinLock
自旋锁 -
os_unfair_lock
互斥锁 -
pthread_mutex
递归锁 -
pthread_mutex
条件锁 -
dispatch_semaphore
信号量 dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized
-
dispatch_barrier_async
栅栏 -
dispatch_group
调度组
性能对比:借用ibireme大神的一张图片
信号量原理
dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量
dispatch_semaphore_create(long value)
和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_
类型的信号量,并且创建的时候需要指定信号量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
等待信号量。如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem)
发送信号量。该函数会对信号量的值进行加1操作。
通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用
dispatch_semaphore_wait
函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal
函数发送信号量(使信号量的值加1),dispatch_semaphore_wait
函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal
函数发送信号量(使信号量的值加1),通知执行下一个任务......如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。
使用
先看加锁,解锁的使用,初始化先设置1,然后每次取钱,存钱之前,都调用dispatch_semaphore_wait
,取钱,存钱之后调用dispatch_semaphore_signal
eg:
YZSemaphore
继承YZBaseLock
,otherTest
里面进行测试
#import "YZSemaphore.h"
@interface YZSemaphore()
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
@end
@implementation YZSemaphore
- (instancetype)init
{
if (self = [super init]) {
self.moneySemaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)__drawMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __drawMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
- (void)__saveMoney
{
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
[super __saveMoney];
dispatch_semaphore_signal(self.moneySemaphore);
}
外部调用的时候,
YZBaseLock *lock = [[YZSemaphore alloc] init];
[lock moneyTest];
输出
iOS-LockDemo[13500:171371] 存10元,还剩110元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
iOS-LockDemo[13500:171369] 取20元,还剩90元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
iOS-LockDemo[13500:171371] 存10元,还剩100元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
iOS-LockDemo[13500:171369] 取20元,还剩80元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
iOS-LockDemo[13500:171371] 存10元,还剩90元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
iOS-LockDemo[13500:171369] 取20元,还剩70元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
iOS-LockDemo[13500:171371] 存10元,还剩80元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
iOS-LockDemo[13500:171369] 取20元,还剩60元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
iOS-LockDemo[13500:171371] 存10元,还剩70元 - <NSThread: 0x600001ca9840>{number = 3, name = (null)}
iOS-LockDemo[13500:171369] 取20元,还剩50元 - <NSThread: 0x600001c960c0>{number = 4, name = (null)}
有结果可知,能保证多线程数据的安全读写。
使用二
信号量还可以控制线程数量,例如初始化的时候,设置最多3条线程
#import "YZSemaphore.h"
@interface YZSemaphore()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation YZSemaphore
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(3);
}
return self;
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
// 线程10、7、6、9、8
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end
调用otherTest
的输出结果为
2018-08-14 16:38:56.489121+0800 iOS-LockDemo[14002:180654] test - <NSThread: 0x600003a938c0>{number = 3, name = (null)}
2018-08-14 16:38:56.492100+0800 iOS-LockDemo[14002:180655] test - <NSThread: 0x600003a93900>{number = 4, name = (null)}
2018-08-14 16:38:56.492281+0800 iOS-LockDemo[14002:180656] test - <NSThread: 0x600003a93940>{number = 5, name = (null)}
2018-08-14 16:38:58.497578+0800 iOS-LockDemo[14002:180657] test - <NSThread: 0x600003a93980>{number = 6, name = (null)}
2018-08-14 16:38:58.499225+0800 iOS-LockDemo[14002:180658] test - <NSThread: 0x600003a8e640>{number = 7, name = (null)}
2018-08-14 16:38:58.549633+0800 iOS-LockDemo[14002:180659] test - <NSThread: 0x600003a93a00>{number = 8, name = (null)}
2018-08-14 16:39:00.499672+0800 iOS-LockDemo[14002:180660] test - <NSThread: 0x600003aa6cc0>{number = 9, name = (null)}
2018-08-14 16:39:00.499799+0800 iOS-LockDemo[14002:180661] test - <NSThread: 0x600003aa6ec0>{number = 10, name = (null)}
2018-08-14 16:39:00.550353+0800 iOS-LockDemo[14002:180662] test - <NSThread: 0x600003aa6d80>{number = 11, name = (null)}
2018-08-14 16:39:02.501379+0800 iOS-LockDemo[14002:180663] test - <NSThread: 0x600003aa6f00>{number = 12, name = (null)}
由结果可知,每次最多三条线程执行。
synchronized
- @synchronized是对mutex递归锁的封装
- 源码查看:objc4中的objc-sync.mm文件
- @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
详细了解可以参考 关于 @synchronized,这儿比你想知道的还要多
使用
@synchronized 使用起来很简单
还使用前面的存钱取票的例子,类YZSynchronized继承自YZBaseLock,代码如下
#import "YZSynchronized.h"
@interface YZSynchronized()
@end
@implementation YZSynchronized
- (void)__saveMoney
{
@synchronized (self) {
[super __saveMoney];
}
}
- (void)__drawMoney
{
@synchronized (self) {
[super __drawMoney];
}
}
@end
调用之后
iOS-LockDemo[2573:45093] 存10元,还剩110元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
iOS-LockDemo[2573:45093] 存10元,还剩120元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
iOS-LockDemo[2573:45093] 存10元,还剩130元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
iOS-LockDemo[2573:45095] 取20元,还剩110元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
iOS-LockDemo[2573:45095] 取20元,还剩90元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
iOS-LockDemo[2573:45095] 取20元,还剩70元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
iOS-LockDemo[2573:45095] 取20元,还剩50元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
iOS-LockDemo[2573:45095] 取20元,还剩30元 - <NSThread: 0x600003e84880>{number = 4, name = (null)}
iOS-LockDemo[2573:45093] 存10元,还剩40元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
iOS-LockDemo[2573:45093] 存10元,还剩50元 - <NSThread: 0x600003ebbb80>{number = 3, name = (null)}
可知,多线程的数据没有发生错乱
源码分析
从runtime源码中的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;
}
// 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;
}
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;
以及
using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
可知@synchronized是对mutex递归锁的封装。因为是递归锁,可以递归加锁,读者有兴趣自行验证。
pthread_rwlock
读写锁
读写锁是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。多读者锁,“push lock”) 用于解决读写问题。读操作可并发重入,写操作是互斥的。
- 需要导入头文件#import <pthread.h>
使用
// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);
eg:
YZRwlock
继承YZBaseLock
,otherTest
里面进行测试,每次读,或者写的之前进行加锁,并sleep 1秒钟,之后解锁,如下所示
#import "YZRwlock.h"
#import <pthread.h>
@interface YZRwlock()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end
@implementation YZRwlock
- (instancetype)init
{
self = [super init];
if (self) {
// 初始化锁
pthread_rwlock_init(&_lock, NULL);
}
return self;
}
- (void)otherTest{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 3; i++) {
dispatch_async(queue, ^{
[self write];
[self read];
});
}
for (int i = 0; i < 3; i++) {
dispatch_async(queue, ^{
[self write];
});
}
}
- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)write
{
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)dealloc
{
pthread_rwlock_destroy(&_lock);
}
@end
调用的时候
YZBaseLock *lock = [[YZRwlock alloc] init];
[lock otherTest];
输出结果为:
2018-08-15 16:07:45.753659+0800 iOS-LockDemo[25457:248359] -[YZRwlock write]
2018-08-15 16:07:46.758460+0800 iOS-LockDemo[25457:248356] -[YZRwlock write]
2018-08-15 16:07:47.763705+0800 iOS-LockDemo[25457:248358] -[YZRwlock write]
2018-08-15 16:07:48.767980+0800 iOS-LockDemo[25457:248381] -[YZRwlock write]
2018-08-15 16:07:49.772241+0800 iOS-LockDemo[25457:248382] -[YZRwlock write]
2018-08-15 16:07:50.777547+0800 iOS-LockDemo[25457:248383] -[YZRwlock write]
2018-08-15 16:07:51.779544+0800 iOS-LockDemo[25457:248359] -[YZRwlock read]
2018-08-15 16:07:51.779544+0800 iOS-LockDemo[25457:248356] -[YZRwlock read]
2018-08-15 16:07:51.779546+0800 iOS-LockDemo[25457:248358] -[YZRwlock read]
由结果可知,打印完write
之后,方法每次都是一个一个执行的,而read
是可以同时执行的,也就是说达到了多读单写的功能。被称为读写锁。
dispatch_barrier_async
异步栅栏
- 这个函数传入的并发队列必须是自己通过
dispatch_queue_cretate
创建的 - 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于
dispatch_async
函数的效果
使用
// 初始化队列
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
// 读
dispatch_async(self.queue, ^{
});
// 写
dispatch_barrier_async(self.queue, ^{
});
eg:
YZBarrier
继承YZBaseLock
,otherTest
里面进行测试
#import "YZBarrier.h"
@interface YZBarrier ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end
@implementation YZBarrier
- (void)otherTest{
// 初始化队列
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 3; i++) {
// 读
dispatch_async(self.queue, ^{
[self read];
});
// 写
dispatch_barrier_async(self.queue, ^{
[self write];
});
// 读
dispatch_async(self.queue, ^{
[self read];
});
// 读
dispatch_async(self.queue, ^{
[self read];
});
}
}
- (void)read {
sleep(1);
NSLog(@"read");
}
- (void)write
{
sleep(1);
NSLog(@"write");
}
@end
调用的时候
YZBaseLock *lock = [[YZBarrier alloc] init];
[lock otherTest];
输出结果为:
2018-08-15 17:50:45.867990+0800 iOS-LockDemo[30046:324146] read
2018-08-15 17:50:46.871969+0800 iOS-LockDemo[30046:324146] write
2018-08-15 17:50:47.876419+0800 iOS-LockDemo[30046:324146] read
2018-08-15 17:50:47.876419+0800 iOS-LockDemo[30046:324148] read
2018-08-15 17:50:47.876450+0800 iOS-LockDemo[30046:324145] read
2018-08-15 17:50:48.880739+0800 iOS-LockDemo[30046:324145] write
2018-08-15 17:50:49.885434+0800 iOS-LockDemo[30046:324145] read
2018-08-15 17:50:49.885435+0800 iOS-LockDemo[30046:324146] read
2018-08-15 17:50:49.885442+0800 iOS-LockDemo[30046:324148] read
2018-08-15 17:50:50.889361+0800 iOS-LockDemo[30046:324148] write
2018-08-15 17:50:51.894104+0800 iOS-LockDemo[30046:324148] read
2018-08-15 17:50:51.894104+0800 iOS-LockDemo[30046:324146] read
由结果可知,打印完write
之后,方法每次都是一个一个执行的,而read
是可以同时执行的,但是遇到写的操作,就会把其他读或者写都会暂停,也就是说起到了栅栏的作用。
dispatch_group_t
调度组
前面说了这么多关于锁的使用,其实调度组也能达到类似栅栏的效果。
api
//1.创建调度组
dispatch_group_t group = dispatch_group_create();
//2.队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//3.调度组监听队列 标记开始本次执行
dispatch_group_enter(group);
//标记本次请求完成
dispatch_group_leave(group);
//4,调度组都完成了
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//执行刷新UI等操作
});
eg:
YZDispatchGroup
继承YZBaseLock
,otherTest
里面进行测试,假设的场景是,需要在子线程下载两个图片,sleep()模拟耗时操作,都下载完成之后,回到主线程刷新UI.
#import "YZDispatchGroup.h"
@implementation YZDispatchGroup
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (void)otherTest{
//1.创建调度组
dispatch_group_t group = dispatch_group_create();
//2.队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//3.调度组监听队列 标记开始本次执行
dispatch_group_enter(group);
dispatch_async(queue, ^{
[self downLoadImage1];
//标记本次请求完成
dispatch_group_leave(group);
});
//3.调度组监听队列 标记开始本次执行
dispatch_group_enter(group);
dispatch_async(queue, ^{
[self downLoadImage2];
//标记本次请求完成
dispatch_group_leave(group);
});
//4,调度组都完成了
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//执行完test1和test2之后,在进行请求test3
[self reloadUI];
});
}
- (void)downLoadImage1 {
sleep(1);
NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
- (void)downLoadImage2 {
sleep(2);
NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
- (void)reloadUI
{
NSLog(@"%s--%@",__func__,[NSThread currentThread]);
}
@end
调用的时候
YZBaseLock *lock = [[YZBarrier alloc] init];
[lock otherTest];
输出结果为:
2018-08-15 19:08:35.651955+0800 iOS-LockDemo[3353:49583] -[YZDispatchGroup downLoadImage1]--<NSThread: 0x6000033ed380>{number = 3, name = (null)}
2018-08-15 19:08:36.648922+0800 iOS-LockDemo[3353:49584] -[YZDispatchGroup downLoadImage2]--<NSThread: 0x6000033e0000>{number = 4, name = (null)}
2018-08-15 19:08:36.649179+0800 iOS-LockDemo[3353:49521] -[YZDispatchGroup reloadUI]--<NSThread: 0x6000033865c0>{number = 1, name = main}
由结果可知,子线程耗时操作,现在图片时候,主线程刷新UI不执行的,等两个图片都下载完成,才回到主线程刷新UI.
dispatch_group
有两个需要注意的地方
- dispatch_group_enter必须在dispatch_group_leave之前出现
- dispatch_group_enter和dispatch_group_leave必须成对出现
自旋锁,互斥锁的选择
前面这么多锁,那么到底平时开发中怎么选择呢?其实主要参考如下标准来选择。
什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
参考资料
关于iOS中的13种加锁方案(上)
给大家推荐一个iOS技术交流群,群内提供数据结构与算法、底层进阶、swift、逆向、底层面试题整合文档等免费资料!!!可以加本人微信拉你进群!
网友评论