背景:在多线程中,如果同时抢一块资源,比如给数组追加数据,可变数组(字典)不是线程安全的,就会导致crash,为了避免这种问题,就衍生出线程同步,来保证线程安全。
先介绍几种锁,
第一种锁
@synchronized(互斥锁)
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要线程同步的操作2");
}
});
1.@synchronized(obj)指令使用的obj为该锁的唯一标识,@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。
2.synchronized 优劣分析:
劣势:@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。开销大,性能差,不适合在生成环境使用。
优点:使用@synchronized关键字可以很方便地创建锁对象,而且不用显式的创建锁对象。
3.synchronized 内部的锁是一个递归锁。
4.atomic:原子属性,为setter方法加锁,的内部实现就是synchronized。
第二种锁
NSLock(性质:互斥锁)
容易造成死锁,比如:
//主线程中
NSLock *theLock = [[NSLock alloc] init];
TestObj *obj = [[TestObj alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void(^TestMethod)(int);
TestMethod = ^(int value){
[theLock lock];
if (value > 0){
[obj method1];
sleep(5); //后面写上[theLock unlock];而不是放在最后,就加锁,解锁就一一对应了,
TestMethod(value-1);
}
[theLock unlock];
};
TestMethod(5);
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[theLock lock];
[obj method2];
[theLock unlock];
});
//简单分析:这段代码是一个典型的死锁情况。在我们的线程中1是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,
//调试器有这些提示:[NSLock lock]: deadlock
//如果在在递归或循环中正确的使用锁:
NSRecursiveLock 递归锁,直接NSLock *theLock = [[NSLock alloc] init];
替换成NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];就ok了
第三种锁
NSConditionLock条件锁
特殊场景使用,也就是满足一定的条件下,才解锁,和创建锁。
第四种方式,用信号量来实现锁的功能
信号量 ==>当信号个数为 0 时,则线程阻塞,等待发送新信号;一旦信号个数大于 0 时,就开始处理任务。
dispatch_semaphore_create、
dispatch_semaphore_wait //在执行完这行代码后,信号量就会减一,如果信号量变成了0,那么就在这里等着,后面一般就是一些排他操作(比如:数组的写操作,保证同步)
dispatch_semaphore_signal、 //作用:在排他操作结束后,现在就可以让其他线程来玩了,执行完这行代码,信号量加1,之前其他等待的线程就又开始工作了。
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作1 开始");
sleep(2); //数组写操作
NSLog(@"需要线程同步的操作1 结束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作2");
dispatch_semaphore_signal(signal);
});
//结果就是:
2016-06-29 20:47:52.324 SafeMultiThread[35945:579032] 需要线程同步的操作1 开始
2016-06-29 20:47:55.325 SafeMultiThread[35945:579032] 需要线程同步的操作1 结束
2016-06-29 20:47:55.326 SafeMultiThread[35945:579033] 需要线程同步的操作2
//这里设置的 dispatch_semaphore_wait(signal, overTime);有个超时时间,一般可以写作DISPATCH_TIME_FOREVER,永不超时。
//这里overtime 如果改成1*NSEC_PER_SEC,那么wait就不等了,就向后走了,
2016-06-30 18:53:24.049 SafeMultiThread[30834:434334] 需要线程同步的操作1 开始
2016-06-30 18:53:25.554 SafeMultiThread[30834:434332] 需要线程同步的操作2
2016-06-30 18:53:26.054 SafeMultiThread[30834:434334] 需要线程同步的操作1 结束
简单总结一下:
互斥锁特点:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。性能最高的锁,OSSpinLock已经不再安全,不再使用。
信号量:一个线程完成了操作完共享数据后,就通过发出信号量告诉别的线程,你们现在可以用这块共享资源了。
性能比较:OSSpinLock > dispatch_semaphore > NSLock > NSRecursiveLock > NSConditionLock > @synchronized.
生产环境一般推荐使用dispatch_semaphore。
上述都是解决同一时刻,只允许一个线程访问某个特定资源的问题,不管是是互斥锁还是信号量,都没法解决另外一个问题:锁定的共享资源会引起读写问题,大多数情况下,限制资源一次只能有一个线程进行读取访问其实是非常浪费的。所以就引出了GCD 的barrier来解决这个问题(资源饥饿)。
GCD barrier 解决资源饥饿 问题
dispatch_barrier使用前提:一定要在一个队列里面(自定义并行队列)。
不能用global queue,因为这个全局队列,每次系统分配可能在不同的队列里面,比如你的barrier在队列A里面,流程走到这,执行自己barrier任务,没用,后面的任务在其他队列做了,不等barrier任务,所以barrier根本不起效果。
剩下就是自定义一个队列,分串行队列,和并行队列,如果是串行队列,就不用了barrier了,他本身就是按照顺序来执行的,加barrier拦截没有意义,所以只能用自定义队列的并行方式。
concurrentQueue =dispatch_queue_create("www.test.com", DISPATCH_QUEUE_CONCURRENT);
- (NSString *)someString{
__weak NSString *localSomeString; //1
dispatch_sync(concurrentQueue, ^{
localSomeString = _someString; //2
});
return localSomeString; //3
}
- (void)setSomeString:(NSString *)someString{
// barrier
dispatch_barrier_async(concurrentQueue, ^{
_someString = someString;
});
}
//当使用并发队列时,要确保所有的 barrier 调用都是 async 的,如果你使用 dispatch_barrier_sync ,那么你很可能会使你自己(更确切的说是,你的代码)产生死锁。
分析:写操作 ,只能用barrier,保证多线程写的安全,读取操作,可以是并行,
dispatch_sync(concurrentQueue, ^{localSomeString = _someString;});
return localSomeString;
这个操作是把block内容放到并行队列里面,这里用dispatch_sync原因就是后面还需要 return localSomeString;也就是必须让他等着,不能block里面赋值还没做,就直接返回了,所以要用dispatch_sync,流程就成了,下面的1,2,3符合逻辑。这里虽然是同步执行,但是任务是放在并行队列里面的,所以任务还是并行执行的。当然,如果这里不是读取操作,是其他操作,不需要后面的返回值,就可以用dispatch_async,异步执行,所以需要看你具体做的事情。
其他:
并行: 自定义并行队列 、 全局队列 ==>放在里面的任务block ,是可以同步执行的
串行: 自定义串行队列 、 主线程 ==> 放在里面的任务block,是一个挨着一个,按照顺序来执行的。
同步:dispatch_sync ==> 就在当前线程做事情
异步:dispatch_async ==> 会开一个新线程 做事情