iOS多线程开发之NSThread&GCD

148 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情

iOS中多线程开发主要用到NSThread、GCD、NSOPeration,这篇文章我们主要介绍NSThread和GCD的使用,下一篇文章再讨论NSOPeration。

NSThread

获取当前线程

[NSThread currentThread];

判断当前线程是否为主线程

[[NSThread currentThread] isMainThread]

模拟耗时操作:延时1秒

[NSThread sleepForTimeInterval:2.0];

线程保活

现在我用如下代码初始化一个NSThread,它start之后就会立即调用初始化传入的threadStart方法。 但是我想让它可以不停地做事情(调用threadRunning),但当我点击屏幕的时候并没有打印"NSThead还在跑"。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.customThread = [[NSThread alloc]initWithTarget:self 
                                               selector:@selector(threadStart) 
                                                 object:nil];
    [self.customThread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSLog(@"尝试使用线程看看可不可以做事情");

    [self performSelector: @selector(threadRunning) onThread:self.customThread withObject:nil waitUntilDone:NO];

    [NSThread sleepForTimeInterval:1];

    NSLog(@"这里还是主线程");
    NSLog(@"1.Runloop信息:%@",[NSRunLoop currentRunLoop]);
}

-(void)threadStart{
    NSLog(@"NSThead启动了");
}

-(void)threadRunning{
    NSLog(@"NSThead还在跑");
}

这就是NSThread难用的地方,start之后调用初始化中传入的selector之后,selector结束之后线程的运行也就会停止,我再想让它做事情,它就不响应了,但如果我想让这个NSThread的线程一直活跃可以响应我的事件该怎么办?

这时候Runloop就派上用场了。 补充一个知识点:每个NSThread都对应一个NSRunloop,我们要让这个Runloop跑起来,这样这个线程就可以一直处在一个事件循环中,监听各种事件,保持对应的NSThread活跃 对NSThread初始化的时候使用的selector做如下修改:

-(void)threadStart{
    NSLog(@"NSThead启动了");
    NSLog(@"2.Runloop信息:%@",[NSRunLoop currentRunLoop]);
    //让Runloop跑起来
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

这样再触摸屏幕threadRunning就可以正常响应了。

GCD

在GCD中有队列和任务两个概念。 队列又分为串行队列和并发队列;任务又分为同步任务和异步任务。 GCD使用就是依靠队列和任务的组合

  • 第一步:创建一个队列
  • 第二步:将一个或多个任务添加到队列中

对这些概念这里就不多做解读了,只做简单的比方。

串行队列

小区要求全员核酸,但只有一个采样员,大家排了很长的队,需要一个一个的采样。这个按顺序一个一个来的队列就叫串行队列。串行队列并不意味着不开启新线程,比如排队的人可以边排队边看手机。

创建一个串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("wo.shi.serialQueue", 
                                                      DISPATCH_QUEUE_SERIAL);

主队列 系统默认提供了一个串行队列就是主队列,所有加入到主队列中的任务都会放到主线程中执行, 获取方法如下:

dispatch_queue_t queue = dispatch_get_main_queue();

主队列阻塞场景举例 比如我们平时写代码如果直接写了一个比较耗时的I/O任务来读一个本地的文件,其实是被默认加入到了主队列中,这会导致主线程发生阻塞,这里我们应当使用CGD的并发队列来处理这个耗时的I/O操作。

并发队列

我们去火车站买火车票,有十几个窗口,这些窗口都有卖火车票,每个窗口前都有很多人在排队买票,这些窗口互不干涉,同时进行,这就叫做并发队列。

创建并发队列

dispatch_queue_t concurrentQueue = dispatch_queue_create("wo.shi.concurrentQueue", 
                                                          DISPATCH_QUEUE_CONCURRENT);

全局并发队列:Global Dispatch Queue 同样的,系统默认也给我们提供了全局的一个并行队列。 获取方法如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

其中DISPATCH_QUEUE_PRIORITY_DEFAULT表示使用默认的优先级,其他优先级定义如下:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2                 //高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0              //默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)               //低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN   //后台运行优先级

同步任务

我煮饭的准备工作分为三步:淘米->将米饭和适量的水加到锅里->开启煮饭模式。这三步都是一环扣一环,做完一步再接着做另一步直到结束。同步任务不会开启新的线程,只能按顺序在当前线程中逐个进行。

将一个同步任务加入某个队列

dispatch_sync(queue, ^{ 
    NSLog(@"这是一个同步执行任务,它被加入到了一个queue的队列中");
});

异步任务

煮饭这个任务,我做完准备工作后其实就交给电饭锅了,这时候我不需要站在锅的旁边一直等它煮完,我可以写会儿代码,或者和老婆吵一架。煮饭这件事在我把它放到锅里那一刻它煮的那个过程就成了一个异步任务,我就可以腾出时间和老婆吵一架了。

将一个异步任务加入到某个队列

dispatch_async(queue, ^{ 
    NSLog(@"这是一个异步执行的任务,它被加入到了一个queue的队列中");
});

队列和执行方式的组合

不同的队列和不同类型的任务可以有多种组合,执行顺序、是否开启线程、遇到的坑我总结到下面的表中: 截屏2022-06-23 10.59.40.png

-(void)test1{

    // 创建一个串行队列

    dispatch_queue_t queueSerial = dispatch_queue_create("test.lock.queue",

                                                          DISPATCH_QUEUE_SERIAL);

    //将同步执行的任务加入到串行队列
    dispatch_sync(queueSerial, ^{
        NSLog(@"1");
    });

    //将同步执行的任务加入到串行队列
    dispatch_sync(queueSerial, ^{
        NSLog(@"2");
    });

    //将异步任务加入到串行队列中
    dispatch_async(queueSerial, ^{
        // 追加任务 3
        NSLog(@"3");
    });

    //将同步执行的任务加入到串行队列
    dispatch_sync(queueSerial, ^{
        NSLog(@"4");
    });

    //将异步任务加入到串行队列中
    dispatch_async(queueSerial, ^{
        // 追加任务 3
        NSLog(@"5");
    });
}

打印结果为:1 2 3 4 5

由此可知在串行队列中无论是加入异步任务还是同步任务都是按加入队列的顺序执行,不同的是,异步任务不会阻塞,同步任务会阻塞当前线程,所以在串行队列中将同步任务嵌套在其他任务中的时候要非常小心,容易出现死锁导致程序崩溃。

GCD死锁举例

死锁报错 Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

死锁状况1: 串行队列中加入 同步任务,同步任务执行的时候又把一个新的同步任务加入到串行队列中。

-(void)test2{
    // 创建一个串行队列
    dispatch_queue_t queueSerial = dispatch_queue_create("test.lock.queue", 
                                                          DISPATCH_QUEUE_SERIAL);
    //在串行队列中加入一个同步任务1
    NSLog(@"1");
    dispatch_sync(queueSerial, ^{
        NSLog(@"2");
        dispatch_sync(queueSerial, ^{
            // 在串行队列中加入一个同步任务2
            NSLog(@"3");
        });
        NSLog(@"4");
    });
}

打印结果是1、2,然后报错 截屏2022-06-23 13.11.09.png 原因分析:

  1. 任务1加入到串行队列中,它执行完成之后才能执行下一步
  2. 在任务1中我们又将任务2加入到了串行队列,而它需要等待任务1的结束
  3. 由于任务2时在任务1中,所以任务1的结束依赖于任务2的结束 于是就形成了任务1和任务2的结束相互依赖,形成了死锁。

拿现在的楼市举例,一个楼盘在建设中(任务1),由于疫情的原因资金链断裂,这时候农民工拿不到薪水,他们就开始讨薪(任务2),楼盘完成建设才能将政府监管账户中的钱拿出来周转,但没钱农民工也遇到生存困境。楼需要赶紧盖完,但民工拿不到薪水就不开工,这就陷入了僵局于从而形成了死锁。

死锁状况2: 串行队列中加入 异步任务,异步任务执行的时候又把同步任务追加到队列中,此时该同步任务是追加在了队列末尾,但是这个异步的完成依赖这个同步任务,这个同步任务又在等异步任务结束。这样就形成了死锁。


-(void)test3{
    // 创建一个串行队列
    dispatch_queue_t queueSerial = dispatch_queue_create("test.lock.queue", 
                                                          DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    //将异步执行的任务加入到串行队列
    dispatch_async(queueSerial, ^{
        NSLog(@"2");
        //将同步任务加入到串行队列中
        dispatch_sync(queueSerial, ^{
            // 追加任务 3
            NSLog(@"3");
        });
    });
    NSLog(@"4");
}

并发队列+同步任务

在并发队列中加入同步任务不会开启新的线程,所以执行顺序是按任务加入的顺序而执行的。

-(void)testConcurrentSync{
    // 并发队列的创建方法
    dispatch_queue_t queueConcurent = dispatch_queue_create("test.ahao.queue", 
                                                             DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<10; i++) {
        dispatch_sync(queueConcurent, ^{
            NSLog(@"并发队列 同步执行 :%@",@(i));
        });
    }
}

打印结果如下: 截屏2022-06-24 22.21.13.png

并发队列+异步任务

这一对组合不会按照加入的顺序执行,会开启新的线程:

-(void)testConcurrentAsync{
    // 并发队列的创建
    dispatch_queue_t queueConcurent = dispatch_queue_create("test.ahao.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queueConcurent, ^{
        // 这里放异步执行任务代码
        NSLog(@"1、并发队列 异步执行---开始%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1、并发队列 异步执行---结束%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queueConcurent, ^{
        // 这里放异步执行任务代码
        NSLog(@"2、并发队列 异步执行---开始%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2、并发队列 异步执行---结束%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queueConcurent, ^{
        // 这里放异步执行任务代码
        NSLog(@"3、并发队列 异步执行---开始%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3、并发队列 异步执行---结束%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queueConcurent, ^{
        // 这里放异步执行任务代码
        NSLog(@"4、并发队列 异步执行---开始%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"4、并发队列 异步执行---结束%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queueConcurent, ^{
        // 这里放异步执行任务代码
        NSLog(@"5、并发队列 异步执行---开始%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"5、并发队列 异步执行---结束%@",[NSThread currentThread]);      // 打印当前线程
    });

}

结果打印如下: 截屏2022-06-24 22.28.08.png

打印顺序不确定,这里面打印了任务所在的线程,发现也是各有不同。

异步+同步执行一组任务

在开发过程中我们有时候会遇到这样的场景:前n个任务执行完成之后才执行最后的任务,比如如果要合成水我们需要制造氧气和制造氢气,这两种气体都制造完成才能合成水,这种场景有两个方案可供选择:

  • 栅栏方法dispatch_barrier_async
  • dispatch_group+dispatch_group_notify配合使用

栅栏方法dispatch_barrier_async

- (void)barrierTest {
    dispatch_queue_t queue = dispatch_queue_create("net.water.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:1]; // 模拟耗时操作
        NSLog(@"获取氢气");      // 打印当前线程
    });

    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:1];              // 模拟耗时操作
        NSLog(@"获取氧气");      // 打印当前线程
    });

    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 3
        NSLog(@"合成水");
    });
}

打印结果如下:

截屏2022-06-24 22.50.58.png

dispatch_group+dispatch_group_notify

- (void)groupNotifyTest {
    //创建一个组
    dispatch_group_t group =  dispatch_group_create();
    //创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("net.water.testQueue", 
                                                    DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_async(group, queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"获取氢气");      // 打印当前线程
    });
    dispatch_group_async(group, queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"获取氧气");      // 打印当前线程
    });
    dispatch_group_notify(group, queue, ^{
        // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
       NSLog(@"生成水");
    });
}

截屏2022-06-24 22.57.25.png

dispatch_group_enter + dispatch_group_leave

在现实开发过程中往往更加复杂,比如我们需要等待几个接口都返回数据之后才能去刷新页面,即异步执行任务中嵌套有异步代码,这时候我们就需要用到 dispatch_group_enter、dispatch_group_leave这对组合来帮助group来判断任务的结束,dispatch_group_enter代表我开始执行一个异步任务,然后在异步任务中的异步代码的回调函数或者block中调用dispatch_group_leave 来表示任务的结束。

/// 获取页面渲染所需网络数据
- (void)prepareDatesource{
    dispatch_queue_t queue =dispatch_get_global_queue(0,0);
    dispatch_group_t group = dispatch_group_create();

    //调用第一个接口获取数据
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        [self fetchAPI01WithCompletion:^{
            dispatch_group_leave(group);
        }];
    });
    
    //调用第2个接口获取数据
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        [self fetchAPI02WithCompletion:^{
            dispatch_group_leave(group);
        }];
    });
    
    //调用第3个接口获取数据
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        [self fetchAPI03WithCompletion:^{
            dispatch_group_leave(group);
        }];
    });

    //所有异步任务都结束
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //已经获取到本页面所有的数据
        [self afterFetchAllData];
    });
}

这样我们就实现了所有接口都请求完毕之后再刷新页面的效果。

GCD其他API

dispatch_once

这个API常用语两个场景 希望只执行一次的代码,比如创建单例和方法交换

  • 单例创建
+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}
  • 方法交换
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector*(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        // 如果originalSelector本身就存在实现则添加不会成功

        BOOL didAddMethod =

            class_addMethod(class,

                originalSelector,

                method_getImplementation(swizzledMethod),

                method_getTypeEncoding(swizzledMethod));

        if(didAddMethod) {

            //如果把自定义的Method的实现和函数签名添加到originalSelector

            class_replaceMethod(class,

                swizzledSelector,

                method_getImplementation(originalMethod),//method_getImplementation(swizzledMethod),

                method_getTypeEncoding(originalMethod));//method_getTypeEncoding(swizzledMethod));

        } else {

            //交换实现方法:VC调用viewWillAppear的时候会跑到xxx_viewWillAppear实现上来
            //xxx_viewWillAppear中必须再调用xxx_viewWillAppear方法,然后就会调用到viewWillAppear
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

dispatch_source_t

可以作为NSTimer的替代方案,他不受Runloop的影响,会更加准确一些,这里就不赘述了。