iOS多线程之同步、依赖

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_asyncdispatch_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

你可能感兴趣的:(iOS多线程之同步、依赖)