美文网首页
聊聊iOS中的多线程

聊聊iOS中的多线程

作者: mai111 | 来源:发表于2020-04-29 23:11 被阅读0次

    当我们在聊iOS中的多线程时,我们主要聊哪些问题?

    多线程的方案有以下几种,分别是c语言的pthread、GCD以及基于这两者进行面向对象封装的NSThread、NSOperation。日常开发中使用比较多的当然是GCD了,不用手动管理线程的生命周期,可以充分利用设备的多核优势,以block的方式来执行任务这些都是GCD的优点。

    GCD

    使用GCD要注意几个概念:

    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    
    • 任务:就是开启GCD时要执行的block里的代码
    • 队列:把任务block放到哪个队列queue执行,队列的执行方式分为串行执行(DISPATCH_QUEUE_SERIAL)和并发执行(DISPATCH_QUEUE_CONCURRENT)
    • sync or async:同步函数或者异步函数,两者最大的区别在于会不会开启新的线程,使用sync函数时,不会开启新的线程,并且加入到队列里的任务会立刻同步执行,换句话说就是会等block的返回,async函数有开启新线程的可能,但具体还要取决于执行任务的队列。

    几种队列和函数结合情况

    死锁

    上经典案例:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"1");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2");
        });
        NSLog(@"3");
    }
    

    这段代码在打印完"1"之后crash了,原因是使用同步函数往主队列里添加任务,会导致主队列卡住,也就是产生了死锁。
    具体来说就是主队列在执行viewDidLoad任务的过程中,往主队列中添加了"2"的任务,因为使用的是同步函数sync,sync函数有个特点就是添加到队列里的任务会立刻执行并等待返回,所以就要求"2"这个任务要立刻在主队列里执行,而主队列是个串行队列,需要执行完当前的viewDIdLoad任务才能往下执行,这就造成了两个任务在互相等待对方的执行完成,形成了死锁。

    对上面的代码稍微做一下修改:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"1");
        dispatch_sync(dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL), ^{
            NSLog(@"2");
        });
        NSLog(@"3");
    }
    

    还是用的同步函数sync,队列也是串行队列,但却没有crash,打印的顺序的是123,这是因为任务"2"是添加到了新创建的串行队列里,主线程遇到sync就立刻执行任务"2"了,执行完再继续执行主队列中的任务。
    so,可以总结一下产生死锁的原因了:使用sync函数往当前串行队列中添加任务,就会产生死锁

    线程安全

    在使用多线程的便利时,也可能会引起线程安全的问题,比如多个线程同时对同一块资源的访问读写行为。
    保证线程同步的方案有很多种,上锁、用GCD的串行队列SerialQueue,设置最大线程并发数量dispatch_semaphore等方式都可以。

    • 自旋锁:等待锁的线程会处于忙等状态
    • 互斥锁:等待锁的线程会处于休眠状态,等待唤醒
    按创建锁的方式来分:
    • OSSpinLock:自旋锁,在iOS10上过期了('OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead)
    • os_unfair_lock:用来替代OSSpinLock的方案
    • pthread_mutex_t:互斥锁,基于C语言的,可以跨平台使用
    • NSLock:对mutex互斥锁的面相对象封装,使用简单
    • @synchronized:是对mutex递归锁的封装,支持递归的方式进行加锁

    不管是用那种方式创建锁,基本都离不开初始化锁、加锁、解锁这几个步骤,使用上也是大同小异。拿NSLock举个栗子:

        NSLock *aLock = [[NSLock alloc] init];
        [aLock lock];
        // 加锁的代码
        [aLock unlock];
    

    有些时候我们是希望读取资源可以允许多条线程并发执行,而写入资源只允许一条线程去执行,并且读写是不能同时进行的,这种多读单写的操作可以用栅栏函数(dispatch_barrier_sync)或者读写锁(pthread_rwlock_t)来处理。
    使用栅栏函数有个要注意的点,用来处理任务的队列必须是自己创建的并发队列,添加到栅栏函数队列里的任务,会在处理这个任务时形成一道栅栏,同一时间只允许线程去处理这个任务,不能用系统提供的全局并发队列,不然就失去栅栏的特性了。

        dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_barrier_async(queue, ^{
            // do something
        });
    

    Runloop

    每条线程都会有一个runloop与之对应,以线程作为key,runloop作为value存储在全局的字典里。主线程的runloop默认是开启的,在UIApplicationMain函数里获取并开启,子线程的runloop是没有开启的,在获取时会创建子线程的runloop,类似懒加载。

        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"%@",[NSThread currentThread]);
        }];
        [thread start];
    

    上面这条线程在处理完block里的任务后就会销毁了,如果有这么一个需求,想固定用一个子线程去处理事情,而不是每次都要重新创建一条新的线程,这就需要用到runloop来使线程保活了。
    要想runloop一直运行不退出,就要确保runloop在当下的运行模式中有Source、Timer或者Observer,下面手动添加一个Source使runloop不退出。

        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"%@",[NSThread currentThread]);
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        }];
        [thread start];
    

    这样这个runloop不会退出,线程是保活了,但也会引起另外的问题,看看苹果对run方法的描述,run方法会反复去调用[runMode: beforeDate:]这个方法,这会开启一个永不销毁的runloop,导致线程也销毁不了,也不是我们想要的,一般来说是想控制runloop在某个时机退出的,而且苹果也举了一个怎样退出runloop的栗子来作参考。


    BOOL shouldKeepRunning = YES; // global
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
    

    通过调用[runMode: beforeDate:]方法来启动runloop,并且在合适的时机把shouldKeepRunning设置为NO,这样调用停掉runloop的方法CFRunLoopStop就可以退出当前的runloop从而使线程不用时销毁。

    参考资料:
    GCD源码: https://github.com/apple/swift-corelibs-libdispatch
    逆向OC类的开源代码:http://www.gnustep.org/resources/downloads.php
    CFRunLoopRef:https://opensource.apple.com/tarballs/CF/

    相关文章

      网友评论

          本文标题:聊聊iOS中的多线程

          本文链接:https://www.haomeiwen.com/subject/lwlbwhtx.html