前言:
在多线程编程中,我们经常使用 各种锁 来保证多线程数据 访问时数据的正确性。其中有 atomic,NSLock,Synchronize,dispatch_semaphore_t,NSCondition,NSConditionLock,NSRecursiveLock(递归锁),pthread_mutex,OSSpinLock,dispatch_barrier_async 这几种。我们对比测试这几种在多线程访问中的性能。
项目整体分析:
DEMO 下载地址 : https://github.com/LoveHouLinLi/LockDemo 喜欢的朋友可以star 一波
1.0 基类分析
基类 BaseControl 生成了访问的数组 NSMutableArray *imageNameArray; 方法 - (void)getIamgeNameWithMutiThread; 多线程访问。
BaseViewController 提供基本的根据Control 初始化的方法。 调用control 的 (void)getIamgeNameWithMutiThread
每个分类 代表一种锁 或者 不加锁 。 根据相应的Item 名称生成对应的 control 。
2.0 主控器 ViewController 点进去 可以点击不同的item 选项测试不同的锁
EntirelyController 是自动 把所有的锁运行一遍 方便 综合对比。
运行对比:
1.0 BaseControl 一次点击可能不会出现错误 ,当点击多次 后出现错误
malloc: *** error for object 0x7f917606ac00: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
说明不加锁的 多线程移除数组操作是 非常危险的 直接造成闪退!
2.0 atomic
/**
新建的一个 atomic 的属性
*/
@property (atomic,strong)NSMutableArray *imageNames;
imageNames 是使用 atomic 修饰的 。 但是 还是出现错误!
Lock_Demo(2234,0x70000378e000) malloc: *** error for object 0x7faeea03d600: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
2017-12-29 16:09:33.629960+0800 Lock_Demo[2234:130868] atomicControl_lock: 0.000139 sec ---- imageNames count:0
Lock_Demo(2234,0x700003894000) malloc: *** error for object 0x7faee9903000: double free
*** set a breakpoint in malloc_error_break to debug
原因是:atomic在set方法里加了锁,防止了多线程一直去写这个property,造成难以预计的数值。但这也只是读写的锁定。跟线程安全其实还是差一些。也就是是赋值property 的时候加锁了。这种情况是 操作这个property 所以并没有用!
3.0 OneThread 和 MainThread
都是单线程 移除的。可以看出 耗时比较高 。但是没有出现错误
4.0 NSLock
使用 NSLock 在getImageName 上加锁 。 这种是 互斥锁 。 效率要高很多
// 重写的方法
- (void)getImageName
{
while (true) {
NSString *imageName;
[lock lock];
// NSLog(@"imageNames count: %ld",imageNameArray.count);
if (imageNameArray.count > 0) {
imageName = [imageNameArray firstObject];
[imageNameArray removeObjectAtIndex:0];
}else{
now = CFAbsoluteTimeGetCurrent();
printf("%30s_lock: %f sec-----imageNames count: %ld\n",[self.title UTF8String] , now-then,imageNameArray.count);
return;
}
[lock unlock];
}
}
5.0 @synchronized()
@synchronized(self){
//需要加锁的 代码 保证安全
}
这个也是类似于互斥锁。 这些都是常用的锁
6.0 dispatch_semaphore_t
dispatch_semaphore_t 是 GCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。 默认创建的信号量是1 。
首先: dispatch_semaphore_create(1); 此时信号量是1
然后 在要加锁的代码前 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 此时信号量为 0
最后 在要解锁的时候 dispatch_semaphore_signal(semaphore); 这样别的线程可以访问 实现多线程安全。
7.0 NSCondition
这个测试 相比 NSLock 加锁的方式。 还多了个有趣的测试 这里面 使用到了 多线程之间的通讯。 也就是多线程中 线程A 和 线程 B的 通讯。在NSCondition 中信号量成为我们通讯的方式。
- (void)newGetImageName:(NSMutableArray *)imageNames
{
// 这并不是一个 while true 循环
NSString *imageName;
[conditionLock lock];
static int m = 0;
static int n = 0;
static int p = 0;
NSLog(@"removeObjectBegin count: %ld\n",imageNameArray.count);
if (imageNames.count>0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
m++;
NSLog(@"执行了%d次删除操作",m);
}else{
p++;
NSLog(@"执行了%d次等待",p);
[conditionLock wait]; //
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
n++;
NSLog(@"执行了%d次继续操作",n);
}
NSLog(@"removeObject count: %ld\n",imageNames.count);
[conditionLock unlock]; //解锁
}
不论imageNames.count 是正还是负 都执行 删除操作 而没有立即崩溃 !!!
这是因为
NSCondition提供更高级的用法。wait和signal,和条件信号量类似。
比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。
但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.
当我用数组做删除试验时,做增删操作并不是每次都会出现,大概3-4次后会出现。单纯的使用lock、unlock是没有问题的。
于是变成了 添加一个 删除一个 每个线程加锁 。 有一点 我们需要注意 [conditionLock broadcast]; 在
API 解析:
- (void)wait;//挂起线程
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal; //任意通知一个线程
- (void)broadcast; //通知所有等待的线程
类似GCD的信号量,wait之后当前线程会被阻塞直到 lock signal。
在用的时候注意,首先对lock对象进行lock
8.0 NSConditionLock
除了 正常的加锁 和 解锁的 用法 如下 所示
同样除了 重写的方法
- (void)getImageName
{
while (true) {
NSString *imageName;
[lock lock];
NSLog(@"imageNames count: %ld",imageNameArray.count);
if (imageNameArray.count > 0) {
imageName = [imageNameArray firstObject];
[imageNameArray removeObjectAtIndex:0];
}else{
now = CFAbsoluteTimeGetCurrent();
printf("%30s_lock: %f sec-----imageNames count: %ld\n",[self.title UTF8String] , now-then,imageNameArray.count);
return;
}
[lock unlock];
}
}
我们可以使用 NSConditionLock 来做一些 多线程交互的操作
在 - (void)createImageName:(NSMutableArray *)imageNames 方法中 我们使用 [lock lockWhenCondition:0];
- (void)createImageName:(NSMutableArray *)imageNames
{
[lock lockWhenCondition:0]; //
static int m = 0; // m
[imageNames addObject:@"0"];
m++;
NSLog(@"添加了%d次",m);
NSLog(@"createImageName count: %ld\n",imageNames.count);
[lock unlockWithCondition:1];
}
// 删除操作
- (void)getImageName:(NSMutableArray *)imageNames
{
NSString *imageName;
// NSConditionLock 的
[lock lockWhenCondition:1];
static int m = 0; //
NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);
if (imageNames.count > 0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
m++;
NSLog(@"执行了%d次删除操作",m);
}
NSLog(@"removeObject count: %ld\n",imageNames.count);
// [lock unlockWithCondition:1]; //
[lock unlockWithCondition:0]; // 解锁 0 的方法 也就是create 的方法
}
整体 多线程 调用的操作
- (void)getIamgeNameWithMutiThread
{
NSMutableArray *imageNames = [NSMutableArray array];
dispatch_group_t dispatchGroup = dispatch_group_create();
__block double then,now;
then = CFAbsoluteTimeGetCurrent(); // 获取绝对时间
for (int i = 0; i<1024; i++) {
dispatch_group_async(dispatchGroup, self.syncronizationQueue, ^{
[self getImageName:imageNames];
});
dispatch_group_async(dispatchGroup, self.syncronizationQueue, ^{
[self createImageName:imageNames];
});
}
//
dispatch_group_notify(dispatchGroup, self.syncronizationQueue, ^{
now = CFAbsoluteTimeGetCurrent();
NSLog(@"%30s_lock: %f sec-----imageNames count: %ld\n",[self.title UTF8String] , now-then,imageNames.count);
});
}
注意 [lock lockWhenCondition:0]; 0 的方法的优先级别 比 1 要高 [lock lockWhenCondition:1]; 1 在 dispatch_group_notify 中调用的方法先调用 0 的方法 后面是1 的方法 改变
于是变成了 创建一张图片 删除 一张图片 但是是多线程操作的 也是线程安全的操作。
9.0 NSRecursiveLock
故名思意 这是 递归锁 。 递归锁 用于递归调用的方法里面。 因为递归调用 加互斥锁 会造成 死锁。
// 获取 imageNames
- (void)getImageName:(NSMutableArray *)imageNames
{
NSString *imageName;
[lock lock];
if (imageNames.count > 0) {
imageName = [imageNames firstObject];
[imageNames removeObjectAtIndex:0];
// 循环删除 这点和
[self getImageName:imageNames];
}
[lock unlock];
}
10.0 pthread_mutex_lock
这也是互斥锁 使用方法
pthread_mutex_t mutex;
第一步 创建 pthread_mutex_init(&mutex, NULL);
第二步 加锁 pthread_mutex_lock(&mutex);
第三步 解锁 pthread_mutex_unlock(&mutex);
第四步 释放锁 这点 和 dispatch_semaphore_t 不一样 这是要我们注意的地方
- (void)dealloc
{
// 销毁 需要手动 销毁 不然有内存泄漏
pthread_mutex_destroy(&mutex);
}
11.0 OSSpinLock
OSSpinLock spinLock;
第一步 初始化锁 spinLock = OS_SPINLOCK_INIT; // 初始化
第二步 加锁 OSSpinLockLock(&spinLock);
第三步 解锁 OSSpinLockUnlock(&spinLock);
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
可以在 Xcode IDE 中看出 在iOS 10.0 上已经被 deprecated 了。
12.0 dispatch_barrier_async/dispatch_barrier_sync
dispatch_barrier_async/dispatch_barrier_sync在一定的基础上也可以做线程同步,会在线程队列中打断其他线程执行当前任务,也就是说只有用在并发的线程队列中才会有效,因为串行队列本来就是一个一个的执行的,你打断执行一个和插入一个是一样的效果。两个的区别是是否等待任务执行完成。
@property (nonatomic,readonly)dispatch_queue_t syncronizationQueue;
dispatch_barrier_async(self.syncronizationQueue, ^{
[self getImageName];
});
参考博客:
https://www.jianshu.com/p/35dd92bcfe8c