多线程一:GCD

OC中常见的多线程方案有以下几种:

技术方案 简介 语言 生命周期 使用频率
pthread 1:一套通用的多线程API
2:适用于Unix \ Linux \ Windows等操作系统
3: 跨平台可移植
4: 使用难度大
C 程序员管理 几乎不用
NSSThread 1:使用更加面向对象
2:可直接操作线程对象
OC 程序员管理 偶尔使用
GCD 1:旨在替换 NSThread
2:充分利用设备的多核
C 自动管理 经常使用
NSOperation 1:基于GCD (底层是GCD)
2:比GCD多了一些更简单使用的功能
3:使用更加面向对象
OC 自动管理 经常使用

GCD在我们项目中用的比较多,所以今天主要研究一下GCD.
GCD有两个用来执行任务的函数:

  1. dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);同步执行
    • 立马在当前线程执行,要求当前任务执行完才能执行下一个任务
    • 不具备开启新线程的能力
  1. dispatch_async(dispatch_queue_t queue, dispatch_block_t block);异步执行
    • 不要求立马执行,可以等一会儿再执行.
    • 具备开启新线程的能力.

GCD中还有两个非常重要的概念任务队列.
而队列可以分为两大类型:

  1. 串行队列
    • 让任务一个一个执行,要求上一个任务执行完才能执行下一个任务.
  2. 并发队列
    • 可以同时执行多个任务 (自动开启多个线程同时执行任务).
    • 并发队列只有在异步函数下才有效.

下面我们就用代码来验证一下上面的几个概念,加深印象.

并发队列在同步函数中无效

如上图所示,我们虽然吧两个任务添加到了全局并发队列中,但是执行的时候并不是同时执行的,而是等任务一执行完毕之后才执行任务二.这就是因为sync不具备开启新线程的能力,即使是并发队列也得在同一个线程中按顺序执行.
如果想让任务一和任务二同时执行,就需要在异步函数中执行任务:
异步并发执行

需要注意的是,如果往主队列中添加任务,即使在异步函数也不会创建新的线程:
主队列

这是因为主队列是一种特殊的串行队列,主队列中的任务会在主线程中执行
同步 , 异步 , 串行 , 并发这几个概念容易混淆,通过表格捋清楚他们的区别:


并发队列 手动创建的串行队列 主队列
同步 (sync) 没有开启新线程
串行执行任务
没有开启新线程
串行执行任务
没有开启新线程
串行执行任务
异步 (async) 开启新线程
并发执行任务
开启新线程
串行执行任务
没有开启新线程
串行执行任务

同步 (sync)函数下,不管是串行队列,并发队列还是主队列,统统不会开启新线程,都是串行执行任务;而即使在异步 (async)函数下,如果是主队列还是不会开启新线程并且串行执行任务.

练习一:以下代码的执行结果:

//以下代码在主线程环境下执行
- (void)viewDidLoad{
    NSLog(@"执行任务1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"执行任务2");
    });
    NSLog(@"执行任务3");
}

以上代码在打印完执行任务1后就崩溃了,因为产生了死锁.产生死锁的原因很简单:因为viewDidLoad是在主线程中执行的,然后又添加了一个任务2到主队列中,并且是同步执行的.我们知道主队列中的任务也是在主线程中执行的.而主线程是串行执行任务的.现在立马要在当前线程 (主线程)中执行任务2,并且任务2执行完毕后才能继续往下执行 (因为是 sync).但是串行要求必须上一个任务执行完后才会执行下一个任务.也就是任务1和任务3都执行完毕后才会执行任务2.而任务3又再等到任务2执行完才能执行.任务2和任务3在互相等待,所以形成了死锁:

死锁

练习二:以下代码的执行结果:

以下代码在主线程环境下运行
- (void)viewDidLoad{
    NSLog(@"执行任务1");
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"执行任务2");
    });
    NSLog(@"执行任务3");
}
//打印结果
2019-12-10 16:40:39.198263+0800 GCD多线程Test[16679:4055662] 执行任务1
2019-12-10 16:40:39.198445+0800 GCD多线程Test[16679:4055662] 执行任务3
2019-12-10 16:40:39.213941+0800 GCD多线程Test[16679:4055662] 执行任务2

这种是不会产生死锁的,因为虽然viewDidLoad中的任务1和任务3是在主线程中执行的,并且任务2也加到了主线程中执行,但是这里调用的是async函数,async函数不会要求立马在当前线程同步执行,它可以等上一个任务执行完毕后再执行下一个任务.

练习三:以下代码的执行结果:

以下代码在主线程环境下运行
- (void)viewDidLoad{
    NSLog(@"执行任务1 - %@",[NSThread currentThread]);
    //串行队列
    dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    //异步执行
    dispatch_async(queue, ^{
        NSLog(@"执行任务2 - %@",[NSThread currentThread]);
        //同步执行
        dispatch_sync(queue, ^{
            NSLog(@"执行任务3 - %@",[NSThread currentThread]);
        });
        NSLog(@"执行任务4 - %@",[NSThread currentThread]);
    });
    NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}

以上代码会在执行任务1,任务5,任务2后产生死锁崩溃.原因如下:

死锁

任务1和任务5被添加到主队列,执行到dispatch_async会创建一个新的子线程,然后把任务2和任务4放入到串行队列.任务2执行完毕后,dispatch_sync把任务3加入到串行队列,并且要求立马在当前线程执行完毕后才能继续往下执行.而任务3要想执行必须等到任务4执行完毕,任务4又在等待任务3执行完毕.最后任务3和任务4互相等待产生死锁.

练习四:以下代码的执行结果:

以下代码在主线程环境下运行
- (void)viewDidLoad{
    NSLog(@"执行任务1 - %@",[NSThread currentThread]);
    //串行队列
    dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    //串行队列
    dispatch_queue_t queue2 = dispatch_queue_create("serial2", DISPATCH_QUEUE_SERIAL);
    //异步执行
    dispatch_async(queue, ^{
        NSLog(@"执行任务2 - %@",[NSThread currentThread]);
        //同步执行
        dispatch_sync(queue2, ^{
            NSLog(@"执行任务3 - %@",[NSThread currentThread]);
        });
        NSLog(@"执行任务4 - %@",[NSThread currentThread]);
    });
    NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}
运行结果:
2019-12-10 17:10:20.164937+0800 GCD多线程Test[16792:4179650] 执行任务1 - {number = 1, name = main}
2019-12-10 17:10:20.165164+0800 GCD多线程Test[16792:4179650] 执行任务5 - {number = 1, name = main}
2019-12-10 17:10:20.165194+0800 GCD多线程Test[16792:4179826] 执行任务2 - {number = 3, name = (null)}
2019-12-10 17:10:20.165326+0800 GCD多线程Test[16792:4179826] 执行任务3 - {number = 3, name = (null)}
2019-12-10 17:10:20.165549+0800 GCD多线程Test[16792:4179826] 执行任务4 - {number = 3, name = (null)}

正常运行,并不会产生死锁,因为任务3被添加到了一个新的队列:


练习五:以下代码的执行结果:

以下代码在主线程环境下运行
- (void)interView05{
    NSLog(@"执行任务1 - %@",[NSThread currentThread]);
    //串行队列1
    dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    //串行队列2
    dispatch_queue_t queue2 = dispatch_queue_create("concurrent", DISPATCH_QUEUE_SERIAL);
    //异步执行
    dispatch_async(queue, ^{
        NSLog(@"执行任务2 - %@",[NSThread currentThread]);
        //同步执行
        dispatch_sync(queue2, ^{
            NSLog(@"执行任务3 - %@",[NSThread currentThread]);
        });
        NSLog(@"执行任务4 - %@",[NSThread currentThread]);
    });
    NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}

运行结果
2019-12-10 17:10:20.164937+0800 GCD多线程Test[16792:4179650] 执行任务1 - {number = 1, name = main}
2019-12-10 17:10:20.165164+0800 GCD多线程Test[16792:4179650] 执行任务5 - {number = 1, name = main}
2019-12-10 17:10:20.165194+0800 GCD多线程Test[16792:4179826] 执行任务2 - {number = 3, name = (null)}
2019-12-10 17:10:20.165326+0800 GCD多线程Test[16792:4179826] 执行任务3 - {number = 3, name = (null)}
2019-12-10 17:10:20.165549+0800 GCD多线程Test[16792:4179826] 执行任务4 - {number = 3, name = (null)}

任务1和任务5被放到主队列中执行,任务2和任务4被放到一个串行队列中执行.执行完任务2后,dispatch_sync会把任务3加入到另一个串行队列,并且要求任务3执行完毕后才能继续往下执行任务4.这里不会产生死锁,因为任务2,3,4并不在同一个串行队列中,任务2和任务4在一个串行队列,任务3在另一个串行队列:

通过上面几个练习,我们现在来总结一下什么情况下才会产生死锁:

使用sync往当前(同一个)串行队列中添加任务,会卡住当前的串行队列,产生死锁.这完全是因为sync串行队列的特性导致的.sync要求执行完当前任务才会继续往下走;串行队列要求上一个任务执行完才能执行下一个任务.新任务等旧任务,旧任务等新任务,就产生了死锁.

练习六:以下代码的执行结果:

以下代码在主线程环境下运行
- (void)viewDidLoad{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"执行任务1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"执行任务2");
    });
}

- (void)test{
    NSLog(@"执行任务3");
}
运行结果:
2019-12-10 17:45:15.301413+0800 GCD多线程Test[16871:4314226] 执行任务1
2019-12-10 17:45:15.301663+0800 GCD多线程Test[16871:4314226] 执行任务2

运行发现只打印了任务1和任务2,任务3没有执行,这是为什么呢?我们把[self performSelector:@selector(test) withObject:nil afterDelay:.0];换成[self performSelector:@selector(test) withObject:nil];再运行一下:

2019-12-10 17:47:05.195349+0800 GCD多线程Test[16889:4327171] 执行任务1
2019-12-10 17:47:05.195508+0800 GCD多线程Test[16889:4327171] 执行任务3
2019-12-10 17:47:05.195640+0800 GCD多线程Test[16889:4327171] 执行任务2

这次三个任务都打印了,这是为什么呢?
我们再把队列和GCD的异步函数先注释掉再运行一下:

以下代码在主线程环境下运行
- (void)viewDidLoad{
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//    dispatch_async(queue, ^{
        NSLog(@"执行任务1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"执行任务2");
//    });
}
运行结果:
2019-12-10 18:00:56.106856+0800 GCD多线程Test[16945:4421406] 执行任务1
2019-12-10 18:00:56.107114+0800 GCD多线程Test[16945:4421406] 执行任务2
2019-12-10 18:00:56.124219+0800 GCD多线程Test[16945:4421406] 执行任务3

也能正常打印,很奇怪.难道这两个方法有什么区别吗?
我们看看- (id)performSelector:withObject的底层:(打开runtime源码 -> 找到NSObject.mm类 -> 找到performSelector方法)

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

而我们在runtime源码中怎么也找不到- (void)performSelector:withObject:afterDelay:方法的实现.我们点击此方法,发现这个方法是在NSRunLoop.h中声明的.

其实- (void)performSelector:withObject:afterDelay:这句代码的本质就是向runloop中添加一个定时器Timmer,由runloop去处理这个定时器事件.所以要想让这个方法执行,必须要有runloop,而我们知道,主线程默认是有runloop的,而子线程默认是没有runloop的,所以任务3始终没有执行.要想让任务3执行,我们就要获取子线程的runloop并且让其运行起来:
- (void)viewDidLoad{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"执行任务1");
        //本质就是向 runloop 中添加 timmer
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"执行任务2");
        //启动 runloop
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1E30, false);
    });
}
运行结果:
2019-12-10 18:19:31.455625+0800 GCD多线程Test[16990:4547398] 执行任务1
2019-12-10 18:19:31.455903+0800 GCD多线程Test[16990:4547398] 执行任务2
2019-12-10 18:19:31.456082+0800 GCD多线程Test[16990:4547398] 执行任务3
总结:在子线程中使用NSTimmer,必须要开启runloop.因为如果不开启runloop,子线程执行完毕后就挂掉了,怎么可能再让子线程在几秒后执行其他任务呢?他都已经挂掉了.

练习七:以下代码的执行结果:

- (void)viewDidLoad{
    NSThread *thread = [[NSThread alloc]initWithBlock:^{
        NSLog(@"执行任务1");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

- (void)test{
    NSLog(@"执行任务2");
}
运行结果
2019-12-10 18:58:40.907272+0800 GCD多线程Test[17500:4757270] 执行任务1
2019-12-10 18:58:40.926970+0800 GCD多线程Test[17500:4756973] *** Terminating 
app due to uncaught exception 'NSDestinationInvalidException', reason: '*** 
-[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: 
target thread exited while waiting for the perform'

执行完任务1后就崩溃了.这也是同样的问题:执行[thread start],thread就会执行block中的任务1,执行完任务1后线程就挂掉了(退出了).这时再让threadperformSelector肯定会报错.

队列组

如果说现在有这么一个需求:异步执行任务1,任务2.等任务1和任务2都执行完毕后再回到主线程执行任务3.如果说用GCD怎么实现呢?这就要用到任务组了:

- (void)viewDidLoad{
    //创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    //创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    //把任务1放到队列组中
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i ++) {
            NSLog(@"执行任务 1 --- %@",[NSThread currentThread]);
        }
    });
    //把任务2放到队列组中
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i ++) {
            NSLog(@"执行任务 2 --- %@",[NSThread currentThread]);
        }
    });
    //notify 等任务1和任务2执行完后再执行任务3
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
            for (int i = 0; i < 5; i ++) {
                NSLog(@"执行任务 3 --- %@",[NSThread currentThread]);
            }
    });
    }

使用多线程的安全隐患 (线程安全)

如果多个线程同时访问同一块数据资源,就很可能发生数据错乱和数据安全的问题.这是我们就要使用线程同步技术.

你可能感兴趣的:(多线程一:GCD)