iOS多线程小结

iOS多线程小结

前言

在系统中,线程是程序真正的执行单元,并负责代码的执行。iOS中主要的技术方案有GCD、NSOperation、NSThread这三种。同时存在线程之间资源的访问、同步的问题,为了保证安全,就需要使用到,本文就从GCDNSOperationNSThread多线程与锁来展开讲解;

GCD

  • 含义:GCD是Apple开发的一个多核编程的解决方法。主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。
  • 主要有以下优点:
    • GCD 可用于多核的并行运算
    • GCD 会自动利用更多的 CPU 内核
    • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
  • 那么这里主要讲解以下几点:
    • 同步/异步 和 串行/并发
    • dispatch_barrier_async:异步栅栏调用,主要用来解决多读单写的问题
    • dispatch_group

同步/异步和串行/并发

  • 可以形成的组合:
    • 同步串行:dispatch_sync(serial_queue, ^{//任务});
    • 异步串行:dispatch_async(serial_queue, ^{//任务});
    • 同步并发:dispatch_sync(concurrent_queue, ^{//任务});
    • 异步并发:dispatch_async(concurrent_queue, ^{//任务});
同步串行
  • 场景一:
- (void)viewDidLoad
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
}

答案:上述代码会产生死锁
  • 死锁原因:

  • 场景二:
- (void)viewDidLoad
{
    dispatch_sync(serial_queue, ^{
        [self doSomething];
    });
}

答案:没有问题
  • 原因:

同步并发
  • 场景
- (void)viewDidLoad
{
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 并发队列
    NSLog(@"1");
    dispatch_sync(global_queue, ^{
        NSLog(@"2");
        dispatch_sync(global_queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

答案:12345
异步串行
  • 场景:
- (void)viewDidLoad
{
    // 开发中频次最高的使用场景
    dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
}

异步并发
  • 场景:
- (void)viewDidLoad
{
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 并发队列
    dispatch_async(global_queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
        NSLog(@"3");
    });
}

- (void)printLog { NSLog(@"2"); }

答案:13
  • 原因:
    • 异步方式分派到全局并发队列,GCD底层维护的线程池中的某一个线程会去执行block;而线程池中的线程默认是不开启RunLoop的,而performSelector:withObject:afterDelay是需要提交到RunLoop上的,所以在上述场景,performSelector会失效。

dispatch_barrier_async()

  • 具体场景:怎样利用GCD实现多读单写?(或者:请设计一个多读单写模型)
    • 需要以下特点
      • 读者、读者并发
      • 读者、写者互斥
      • 写者、写者互斥
  • 实现:

  • 解决方案:dispatch_barrier_async(concurrent_queue, ^{//写操作});
// NSMutableDictionary *mutDic; // 数据容器

// 读操作
- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取数据:数据需要实时返回
    dispatch_sync(concurrent_queue, ^{
        obj = [mutDic objectForKey:key];
    });
    return obj;
}

// 写操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [mutDic setObject:obj forKey:key];
    });
}

dispatch_group()

  • 使用场景:使用GCD实现这个需求:A、B、C三个任务并发,完成后执行任务D?
    • 解决方案:dispatch_group_async()
  • 如:多图下载,拼合成一张图
NSArray *urls = @[@"1.jpg", @"2.jpg"];
    
    dispatch_group_t group = dispatch_group_create();
    for (NSString *path in urls) {
        // 异步组分派s到并发队列中
        dispatch_group_async(group, concurrent_group, ^{
            // 根据图片url去下载图片
            NSLog(@"path = %@", path);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 当添加到组的所有任务执行完成后会调用改block
        NSLog(@"所有图片已全部下载完成");
    });

NSOperation

  • NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
  • 有哪些优势和特点:
    • 添加任务依赖:方便的控制任务执行顺序
    • 任务执行状态控制
    • 可以设置最大并发量

任务执行状态控制:有哪些?

  • isReady:是否处于就绪状态

  • isExecuting:是否处于执行中的状态

  • isFinished:是否已执行完成

  • isCancelled:是否已取消

状态控制:怎样控制NSOperation的任务状态

  • 主要看我们是否重写了NSOperation的startmain方法
    • 如果只重写main方法,那么底层控制变更任务执行完成状态,以及任务退出。
    • 如果重写了start方法,则需要自行控制任务状态
      • 在源码的分析中,在start方法中,系统做了状态判断和控制,并通过KVO的方式,告知NSOperationQueue这个监听对象。

系统是怎样移除一个isFinished=YES的NSOperation的?

  • 答案:通过KVO(会拓展KVO的了解)

NSThread

  • 首先我们来看下NSThread的启动流程

  • Q:那么NSThread的内部实现机制是怎样的(即start方法的实现过程)
    • 在start方法中,会调用pthread_create()来创建线程,指定线程的启动函数
    • 在线程的启动函数中,首先发送通知,线程已经启动了,再调用main方法,最后调用exit,关闭线程
    • 在main中,会调用target中的selector方法,来执行创建Thread的时候指定的selector方法

    • 可以通过NSThread的detachNewThreadSelector:toTarget:withObject来传递Target和selector方法;也可以通过初始化方法initWithTarget:selector:object来传递Target和selector方法;
  • Q:怎样通过NSThread实现常驻线程?
    • 为什么需要实现常驻线程?
      • 由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用。常驻线程可以减少消耗cpu。
    • 解决方案:
      • 需要在创建NSThread的时候传递进来的selector方法中,去维护一个RunLoop事件循环

  • iOS当中都有哪些锁呢?(你在日常开发中都使用过哪些锁)
    • @synchronized
      • 业务场景:创建单例对象;NSMutableArray的赋值(多图上传)
    • atomic :
      • 属性关键字
      • 对修饰的对象赋值操作是保证线程安全的,但对象的使用操作是不保证线程安全的;
      • 如 @property(atomic) NSMutableArray *array; - self.array = [NSMutableArray array]; 是线程安全的
        • 如NSMutableArray的addObject; 就不是线程安全的
    • OSSpinLock :自旋锁,特点如下:(在内存管理中,SideTable中有应用到)
      • 循环等待询问,不释放当前资源;
      • 用于轻量级数据访问
    • NSLock
      • 使用lock和unlock方法进行加锁和解锁操作
    • 同一把锁,重入加锁,会导致死锁
      • 解决方案:使用递归锁NSRecursiveLock,特点是可以重入
    • NSRecursiveLock:递归锁,特点是可以重入

    • dispatch_semaphore_t:GCD信号量,主要考察线程同步的理解,来解决一些线程同步问题
  • NSLock死锁场景:

  • 解决方案:

dispatch_semaphore_t

  • 相关的实现方法:
    • dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 创建信号量
    • dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
    • dispatch_semaphore_signal(semaphore); // 发送信号量

内部实现

  • dispatch_semaphore_create
    • semaphore的结构
      struct semaphore {
        int value;
        List <thread>;
      }
      
  • dispatch_semaphore_signal
{
	S.value = S.value + 1;
	if S.value <= 0 then wakeup(S.List); // 若小于等于0意味着在释放钱有队列在排队,列表当中有线程需要唤醒;唤醒是一个被动行为
}
  • dispatch_semaphore_wait
{
	S.value = S.value - 1;
	if S.value < 0 then Block(S.List); // 小于0意味着我们不能获取信号量,当前的线程主动阻塞起来。阻塞是一个主动行为
}

多线程总结

  • 怎样用GCD实现多读单写?(dispatch_barrier_async)
  • iOS系统为我们提供的几种多线程技术各自的特点是怎样的?(结合业务场景)
    • GCD:一般在简单的线程同步,子线程的分派,资源同步的场景使用
    • NSOperation:在AFN、SDWebImage的内部都有实现,主要是考虑其可以添加任务依赖,控制状态
    • NSThread体现在常驻线程的使用中
  • NSOperation对象在Finished之后是怎样从queue当中移除掉的?(通过KVO通知queue)
  • 你都用过哪些锁?结合实际谈谈你是怎样使用的?(根据实际业务场景,可举例NSLock死锁问题)

参考文章

最后

如果对大家有帮助,请github上follow和star,本文发布在戴超的技术博客,转载请注明出处

Loading Disqus comments...
Table of Contents