iOS常见的保持数据同步机制:
-
os_unfair_lock
用来代替OSSpinLock自旋锁 -
OSSpinLock
--- 自旋锁会循环等待访问,不释放当前资源
多用于轻量级的数据访问,比如简单的int值加减操作。可能会出现优先级反转问题,弃用。 -
dispatch_semaphore_t
--- 信号量,可以保证最大并发数 -
pthread_mutex
跨平台锁 -
dispatch_queue(DISPATCH_QUEUE_SERIAL)
串行队列 -
NSLock
是对pthread_mutex
的面向对象封装 -
NSCondition
--- 也是对pthread_mutex
的面向对象封装,使用pthread_cond_init
的时候,可以设置条件,可以用于生产者消费者模型 pthread_mutex(recursive)
-
NSRecursiveLock
--- 递归锁,也是对pthread_mutex
的面向对象封装,使用pthread_mutexattr_settype
的时候,可以选择创建递归锁 -
NSConditionLock
条件和锁混在一起,既可以加锁解锁,也可以等待条件唤醒锁;还可以保证线程按顺序执行 -
@synchronized
--- 同步锁,使用最简单,但是性能是最差的
注意:以上顺序也是按照性能从高到低⬇️排序
pthread_mutex
初始化使用:
@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, assign) pthread_cond_t cond;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); # 设置成递归锁
# 初始化锁
pthread_mutex_init(&_mutex, &attr);
# 销毁属性
pthread_mutexattr_destroy(&attr);
# 初始化条件
pthread_cond_init(&_cond);
NSLock
iOS中对于资源抢占的问题可以使用同步锁NSLock来解决。
使用时把需要加锁的代码放到NSLock的lock和unlock之间,一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。
另外,对于被抢占资源来说将其定义为原子属性是一个很好的习惯,因为有时候很难保证同一个资源不在别处读取和修改。nonatomic属性读取的是内存数据(寄存器计算好的结果),而atomic就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只有一个线程在访问一个属性。
#pragma mark 请求图片数据
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
//加锁
[_lock lock];
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
//使用完解锁
[_lock unlock];
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
NSCondition 线程通信处理
NSCondition继承NSLocking协议,所以也具有最基本的lock和unlock功能。
NSCondition更重要的是解决线程之间的调度关系(当然,这个过程中也必须先加锁、解锁)。NSCondition可以调用wati方法控制某个线程处于等待状态,直到其他线程调用signal或者broadcast方法唤醒该线程才能继续。
lock: 一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问
unlock :与lock 同时使用
wait:让当前线程处于等待状态
signal:CPU发信号告诉线程不用在等待,可以继续执行线程通信处理,此方法唤醒一个线程,如果有多个线程在等待则任意唤醒一个
broadcast:CPU发信号告诉线程不用在等待,可以继续执行线程通信处理,此方法会唤醒所有等待线程
使用场景:当接受到图片消息的时候,需要异步下载,等到图片下载完成之后,同步数据库,方可通知前端更新UI。此时就需要使用NSCondition 的wait
-(void)testConditionLock {
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *allFoods = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
if (allFoods.count <= 0) {
[condition wait];
[allFoods removeLastObject];
NSLog(@"食用食物:%ld",allFoods.count);
}
[condition unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
[allFoods addObject: @"food"];
NSLog(@"增加食物: %ld",allFoods.count);
[condition signal];
[condition unlock];
});
}
NSConditionLock保证线程顺序:
NSConditionLock:
条件锁,一个线程获得了锁,其它线程等待。
[xxxx lock];
表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁
[xxx lockWhenCondition:A条件];
表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
[xxx unlockWithCondition:A条件];
表示释放锁,同时把内部的condition设置为A条件
synchronized代码块
使用@synchronized解决线程同步问题相比较NSLock要简单一些,日常开发中也更推荐使用此方法。
首先选择一个对象作为同步对象(一般使用self),然后将”加锁代码”(争夺资源的读取、修改代码)放到代码块中。@synchronized中的代码执行时先检查同步对象是否被另一个线程占用,如果占用该线程就会处于等待状态,直到同步对象被释放。
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
//线程同步
@synchronized(self){
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[NSThread sleepForTimeInterval:0.001f];
[_imageNames removeObject:name];
}
}
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
GCD信号量semaphore
- (void)initSemaphore {
/*初始化信号量
参数是信号量初始值
*/
_semaphore=dispatch_semaphore_create(1);
// 省略代码...调用请求图片数据
}
#pragma mark 请求图片数据
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
/*信号等待 信号量是否大于0 如果大于0则减一 然后继续向下执行,否则等待
第二个参数:等待时间
*/
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
//信号通知 信号量加1
dispatch_semaphore_signal(_semaphore);
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
保持数据同步的方式:
- 并发访问 数据拷贝
记录主线程删除操作,在子线程将要同步数据给主线程前,先同步执行删除操作再提交数据给主线程。
弊端:数据拷贝,占用内存。
- 串行访问
在串行子线程中执行数据请求、数据删除操作,如果串行队列中已经有任务,则等待执行完毕再执行下一个任务。
弊端:串行耗时。
解决网络请求的依赖关系:
比如一个接口的请求,依赖于另一个请求的结果
- 思路1:
使用NSOperation的addDependecy
解决,但是网络请求多是异步请求,无法保证回调时候再开启下一个网络请求 ❌ - 思路2:
在上一个网络请求回调中再开启下一个网络请求,但是可能拿不到回调,无法开启下一个网络请求 ❌ - 思路3:
使用GCD组队列中的dispatch_group_async
和dispatch_group_notify
✔️
dispatch_queue_t concurent_t = dispatch_queue_create("com.zmj", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group_t = dispatch_group_create();
dispatch_group_async(group_t, concurent_t, ^{
// 请求1
});
dispatch_group_notify(group_t, dispatch_get_main_queue(), ^{
// 请求2
});
- 思路4:
使用GCD的栅栏dispatch_barrier_async
✔️
dispatch_queue_t concurent_t = dispatch_queue_create("com.zmj", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurent_t, ^{
// 请求1
});
dispatch_barrier_async(concurent_t, ^{
// 请求2
});
- 思路5:
使用GCD的信号量dispatch_semaphore
dispatch_queue_t concurent_t = dispatch_queue_create("com.zmj", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t sem_t = dispatch_semaphore_create(0);
dispatch_async(concurent_t, ^{
// 请求1结束后 增加信号个数
dispatch_semaphore_signal(sem_t);
});
dispatch_async(concurent_t, ^{
// 无线等待 直到请求1结束 增加了信号量大于0后 才开始
dispatch_semaphore_wait(sem_t, DISPATCH_TIME_FOREVER);
// 请求2开始
});
知识补充
死锁产生的条件:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
文字拓展:https://github.com/bestswifter/blog/blob/master/articles/ios-lock.md