iOS-多线程1-GCD

首先看一下iOS中多线程的实现方案

多线程方案.png
  1. 这些多线程方案的底层都是依赖pthread
  2. NSThread线程生命周期是程序员管理,GCD和NSOperation是系统自动管理
  3. NSThread和NSOperation都是OC的,更加面向对象
  4. NSOperation基于CGD,使用更加面向对象

一. GCD的简单使用

1. 同步、异步、串行、并发

先理解比较容易混淆的术语:同步、异步、串行、并发

同步(sync)和异步(async)主要影响:能不能开启新的线程

同步(sync):在当前线程中执行任务,不具备开启新线程的能力
异步(async):在新的线程中执行任务,具备开启新线程的能力

并发和串行主要影响:任务的执行方式

串行:一个任务执行完毕后,再执行下一个任务
并发:多个任务并发(同时)执行

2. GCD常用函数

GCD是开源的,源码在:https://github.com/apple/swift-corelibs-libdispatch

GCD中有两个用来执行任务的函数,queue是队列,block是要执行的任务,如下:

  1. 用同步的方式执行任务:

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

  1. 用异步的方式执行任务:

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_sync不具备开启线程的能力,它的任务在当前线程执行的。
dispatch_async具备开启线程的能力,它的任务在子线程执行的。

3. GCD的队列

GCD的队列可以分为两大类型

  1. 串行队列(Serial Dispatch Queue)
    让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
  2. 并发队列(Concurrent Dispatch Queue)
    可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    并发功能只有在异步(dispatch_async)函数下才有效

总结:

dispatch_sync和dispatch_async函数决定是否在当前线程执行,串行队列和并发队列决定任务是串行执行还是并发执行,他们决定的东西互不影响。

4. GCD函数和队列的组合

先看结论:

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

① 全局并发队列

- (void)viewDidLoad
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
        }
    });
}
    
打印:
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行

可以发现,任务1和任务2同时执行,任务1在子线程执行,任务2在当前主线程执行,任务1和任务2是并发的。

② 串行队列

- (void)viewDidLoad
{
    NSLog(@"任务000");

    dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
        }
    });

    NSLog(@"任务333");
}

打印:
NSLog(@"任务000");
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
NSLog(@"任务333");

可以发现,任务1先执行,任务2后执行,任务1在子线程执行,任务2在当前主线程执行,任务1和任务2是串行的。

注意:虽然上面有使用create,但是在ARC中不用写dispatch_release(queue),因为ARC已经帮你做过了(CF开头的该写还是要写)。

③ 主队列(是一种特殊的串行队列)

- (void)viewDidLoad
{
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
        }
    });
}

打印:
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行

注意:上面都是使用dispatch_async函数,如果在主队列中使用dispatch_sync函数会产生死锁,报错:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

上面说了dispatch_async具备开启新线程的能力,但是并不代表它一定要开启新线程。比如上面,使用dispatch_async传入主队列,因为放到主队列里面的所有东西都会在主线程执行,所以就没开启新线程。

可以发现,任务1先执行,任务2后执行,任务1和任务2都在主线程执行,任务1和任务2是串行的。

5. 关于死锁

首先要明白两个特点:

  1. 队列的特点:FIFO (First In First Out),先进先出
  2. dispatch_sync函数的特点:sync函数要求立马在当前线程同步执行任务

先看结论:

  1. 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
  2. 两个关键点:sync函数,当前串行队列
  3. 上面②中,没有产生死锁,是因为使用sync函数添加任务到自己创建的串行队列中了,并没有添加到当前的串行队列(主队列),所以不会产生死锁。
  4. 上面③中,如果把其中一个async换成sync就会产生死锁,因为主队列是当前串行队列,使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列,也就是会卡住主队列。
  5. 下面问题1,同上面4
  6. 下面问题3,当前串行队列是myqueu,再往myqueu队列里面添加sync任务,就会卡住myqueu队列。

问题1:

以下代码是在主线程执行的,会不会产生死锁?会!

- (void)viewDidLoad
{
    NSLog(@"执行任务1%@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
    //报错:Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    NSLog(@"执行任务2%@",[NSThread currentThread]);
    });

    NSLog(@"执行任务3%@",[NSThread currentThread]);
}

打印:

2020-09-17 10:05:57.539001+0800 GCD[49304:3047986] 执行任务1{number = 1, name = main}
(lldb) 

可能你会想dispatch_sync不具备开启新线程的能力,那就在主线程执行,正好传入的是主队列,那就先执行任务1再执行任务2再执行任务3不就好了,而实际情况为什么会死锁呢?

死锁的原因:由于队列先进先出,要想执行任务3,就要任务2先执行完。要想执行任务2,就要viewDidLoad这个大任务先执行完,viewDidLoad要执行完,就要等任务3先执行完。最后任务3等任务2,任务2等任务3,他们就这样互相卡住,产生死锁。

图示如下:

死锁.png

问题2:

以下代码是在主线程执行的,会不会产生死锁?不会!

- (void)viewDidLoad
{
    NSLog(@"执行任务1%@",[NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2%@",[NSThread currentThread]);
    });

    NSLog(@"执行任务3%@",[NSThread currentThread]);
}

dispatch_async函数特点:不要求立马在当前线程同步执行任务

打印结果:

执行任务1{number = 1, name = main}
执行任务3{number = 1, name = main}
执行任务2{number = 1, name = main}

可以发现,任务1 3 2按顺序在主线程执行,任务2会在任务3执行完成后再执行,自己可分析上图理解。

问题3:

以下代码是在主线程执行的,会不会产生死锁?会!

- (void)viewDidLoad
{
    NSLog(@"执行任务1%@",[NSThread currentThread]);

    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    
    //下面这个dispatch_async函数,我们把它称为大任务
    dispatch_async(queue, ^{ // block0
        NSLog(@"执行任务2%@",[NSThread currentThread]);
        
        dispatch_sync(queue, ^{ // block1
            NSLog(@"执行任务3%@",[NSThread currentThread]);
        });
    
        NSLog(@"执行任务4%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5%@",[NSThread currentThread]);
}

分析代码,首先肯定会先在主线程执行任务1,然后大任务是在子线程串行执行,任务5在主线程串行执行,所以打印结果:

可能是:
执行任务1{number = 1, name = main}
执行任务5{number = 1, name = main}
执行任务2{number = 3, name = (null)}
(lldb) 

也可能是:
执行任务1{number = 1, name = main}
执行任务2{number = 3, name = (null)}
执行任务5{number = 1, name = main}
(lldb) 

这取决于任务5和子线程的大任务谁先执行,是不确定的。

现在就知道为什么死锁了,首先外面的大任务先称为block0,里面的小任务先称为block1。系统会先把block0添加到串行队列中,再把block1添加到串行队列中。先执行完block0,但是block0要执行完就要执行block1,但是block1是后添加的,执行block1要先执行完block0,他们就这样卡着,产生死锁。

图示如下:

死锁2.png

问题4:

以下代码是在主线程执行的,会不会产生死锁?不会!

- (void)viewDidLoad
{
    NSLog(@"执行任务1%@",[NSThread currentThread]);
    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    //创建全局并发队列
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
    //创建串行队列
    //dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{ // block0
        NSLog(@"执行任务2%@",[NSThread currentThread]);
        
        dispatch_sync(queue2, ^{ // block1
            NSLog(@"执行任务3%@",[NSThread currentThread]);
        });
        
        NSLog(@"执行任务4%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5%@",[NSThread currentThread]);
}

分析代码,首先肯定在主线程执行任务1,之后block0会在子线程执行,任务5会在主线程执行,但是block0和任务5谁先执行说不准。

当执行到block0的时候,执行到任务3会先去另外一个串行队列执行block1,之后再执行任务4。虽然和上面那个死锁代码差不多,但是因为他们不在同一个队列,所以不会死锁,图示如下:

不死锁3.png

打印结果:

可能是:
执行任务1{number = 1, name = main}
执行任务5{number = 1, name = main}
执行任务2{number = 3, name = (null)}
执行任务3{number = 3, name = (null)}
执行任务4{number = 3, name = (null)}

也可能是:
12534
12354(需要打断点才可以打印出来)
12345

同理,如果把上面的queue2换成串行队列,这时候还是不会产生死锁,图示如下:

不死锁4.png

问题5:

以下代码是在主线程执行的,会不会产生死锁?不会!

- (void)viewDidLoad
{
    NSLog(@"执行任务1%@",[NSThread currentThread]);

    //创建全局并发队列
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{ // block0
        NSLog(@"执行任务2%@",[NSThread currentThread]);
        
        dispatch_sync(queue, ^{ // block1
            NSLog(@"执行任务3%@",[NSThread currentThread]);
        });
        
        NSLog(@"执行任务4%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5%@",[NSThread currentThread]);
}

这时候,block0和block1都在全局并发队列里面,执行到任务3要执行block1,既然要执行block1那就执行吧,反正任务是并发,可以同时执行。

如下图:

不死锁5.png

任务1 5在主线程执行,任务2 3 4在同一个子线程执行,任务1最先执行,任务2 3 4按顺序执行,任务5穿插其中,所以有四种情况,如下:

//执行任务1{number = 1, name = main}
//执行任务5{number = 1, name = main}
//执行任务2{number = 5, name = (null)}
//执行任务3{number = 5, name = (null)}
//执行任务4{number = 5, name = (null)}

//执行任务1{number = 1, name = main}
//执行任务2{number = 3, name = (null)}
//执行任务3{number = 3, name = (null)}
//执行任务4{number = 3, name = (null)}
//执行任务5{number = 1, name = main}

//执行任务1{number = 1, name = main}
//执行任务2{number = 6, name = (null)}
//执行任务3{number = 6, name = (null)}
//执行任务5{number = 1, name = main}
//执行任务4{number = 6, name = (null)}

//执行任务1{number = 1, name = main}
//执行任务2{number = 5, name = (null)}
//执行任务5{number = 1, name = main}
//执行任务3{number = 5, name = (null)}
//执行任务4{number = 5, name = (null)}

补充:获取的全局并发队列和创建的全局并发队列的区别

- (void)viewDidLoad
{
    dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"%p %p %p %p", queue1, queue2, queue3, queue4);
}

打印:
0x10db71e80 0x10db71e80 0x600000f81480 0x600000f81400

可以发现:

  1. 两种方式都可以获取全局并发队列
  2. 使用系统获取的一直是同一个全局并发队列,自己创建的每个全局并发队列都不一样

6. 面试题

1. 下面代码打印什么?

- (void)test2
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1");
        // 这句代码的本质是往Runloop中添加定时器
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
}

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

运行后发现打印:1 3。

为什么打印:1 3,不打印2,为什么?
因为performSelector: withObject: afterDelay:的本质是往Runloop中添加定时器,属于Timers事件,子线程默认没有开启RunLoop,所以定时器不会执行,所以打印1 3。

可能有人会问,子线程的Runloop没有开启,那么为什么同样是在子线程的1 3可以打印呢?
其实1 3和子线程的RunLoop没有关系,1 3是普通的代码,会立马执行,2是定时器,会添加到RunLoop中。

如果把子线程的RunLoop运行起来:

- (void)test2
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1");
        // 这句代码的本质是往Runloop中添加定时器,属于Timers事件。
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

        NSLog(@"4");
    });
}

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

打印:1 3 2,这时候定时器添加RunLoop了,test方法会在下一次RunLoop循环中处理,所以打印结果是1 3 2。又因为RunLoop添加了addPort这时候RunLoop会一直在工作,所以不会走到4。

如果把上面代码:[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]注释掉,会打印:1 3 2 4,这是因为RunLoop执行完2的定时器事件就没事了,RunLoop就会退出,这样就会执行4。

同理,如果将上面代码在主线程里面执行:

- (void)test2
{
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        NSLog(@"1");
        // 这句代码的本质是往Runloop中添加定时器,属于Timers事件。
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        
        NSLog(@"3");
    });
}

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

打印:1 3 2,因为主线程的RunLoop默认自动开启的,所以会打印1 3 2。

如果将performSelector:withObject:afterDelay:换成performSelector:withObject:呢?
我们在NSObject.mm里面查看performSelector:withObject:afterDelay:源码:

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

可以发现performSelector:withObject:的内部直接就是objc_msgSend,所以就相当于方法调用,和RunLoop无关,所以就一行一行执行就好了。

objc4源码里面没找到performSelector:withObject:afterDelay:的内部实现,只知道它定义在RunLoop.h里面了,说明它是RunLoop的东西。
如果想要看performSelector:withObject:afterDelay:的底层实现要先了解一下GNUstep。

GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:http://www.gnustep.org/resources/downloads.php
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值

在上面地址找到GNUstep Base,发现它是Foundation框架的,如下图:

GNUstep Base.png

找到Foundation框架下的NSRunLoop.m文件,搜索“performSelector”,找到如下实现:

- (void) performSelector: (SEL)aSelector
          withObject: (id)argument
          afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop     *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer  *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
                         target: self
                       argument: argument
                          delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

可以发现,首先创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面。可以知道,performSelector:withObject:afterDelay:里面是有创建RunLoop的,但是没有运行RunLoop,所以上面的代码才会那样。

还找到run方法的内部实现:

- (void) run
{
  [self runUntilDate: theFuture];
}

- (void) runUntilDate: (NSDate*)date
{
  BOOL      mayDoMore = YES;

  /* Positive values are in the future. */
  while (YES == mayDoMore)
    {
      mayDoMore = [self runMode: NSDefaultRunLoopMode beforeDate: date];
      if (nil == date || [date timeIntervalSinceNow] <= 0.0)
        {
          mayDoMore = NO;
        }
    }
}

可以发现run方法内部的确使用while循环一直在调用runMode:beforeDate:,这和线程保活里面说的一致。

总结:

  1. performSelector:withObject:的内部直接就是objc_msgSend
  2. performSelector:withObject:afterDelay:的内部会创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面,但是并没有运行RunLoop
  3. run方法内部的确使用while循环一直在调用runMode:beforeDate:

2. 下面代码打印什么?

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");

    }];
    [thread start];

    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

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

运行代码,打印1之后报错:

1
-[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'

翻译过来:当执行performSelector的时候,目标线程已经退出。

这是因为 [thread start]之后就会执行block,然后打印1,打印完成线程就退出了,这时候再去这个线程上做事情就会报上面的错。

解决办法如下,给这个线程添加RunLoop,再给RunLoop添加端口事件,让RunLoop一直有事做,这样线程就不会退出了,如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");

        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];

    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

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

上面代码添加RunLoop之后,线程就一直有事情做了。当打印完1,线程就开始休眠,这时候执行performSelector方法,就是给线程发送一个消息,然后线程被唤醒,开始执行test方法。运行代码,打印:1 2。

二. 延迟执行

iOS常见的延时执行有两种方式:

1. 调用NSObject的方法

[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 2秒后再调用self的run方法

该方法在哪个线程调用,那么run就在哪个线程执行,通常是主线程,代码如下:

@implementation YYViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"打印线程----%@",[NSThread currentThread]);
    //在主线程中执行run方法
    [self performSelector:@selector(run) withObject:nil afterDelay:2.0];
}

- (void)run
{
    NSLog(@"延迟执行----%@",[NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_queue_create("wendingding", 0);
    //在子线程中执行test方法
    dispatch_async(queue, ^{
        [self performSelector:@selector(test) withObject:nil afterDelay:1.0];
    });
    NSLog(@"异步函数");
}

- (void)test
{
    NSLog(@"子线程中执行----%@",[NSThread currentThread]);
}
@end

2. 使用dispatch_after函数

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});

传入一个队列,如果是主队列,那么就在主线程执行,如果是并发队列,那么会新开启一个线程,在子线程中执行,代码如下:

@implementation YYViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSLog(@"打印当前线程---%@",  [NSThread currentThread]);

    //传入主队列,在主线程执行
    dispatch_queue_t queue= dispatch_get_main_queue();
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"主队列--延迟执行------%@",[NSThread currentThread]);
    });

    //全局并发队列,在子线程执行
    //1.获取全局并发队列
    dispatch_queue_t queue1= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //2.计算任务执行的时间
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
    //3.会在when这个时间点,执行queue中的这个任务
    dispatch_after(when, queue1, ^{
        NSLog(@"并发队列-延迟执行------%@",[NSThread currentThread]);
    });
}
@end

三. 一次性代码

使用dispatch_once函数能保证某段代码在程序运行过程中只被执行一次,如下:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

四. GCD队列组

思考:如何用GCD实现以下功能?
异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3。

代码如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 添加异步任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });
    
    // 添加异步任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });

    // 等前面的任务执行完毕后,会在主线程自动执行这个任务
    dispatch_group_notify(group, queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务3-%@", [NSThread currentThread]);
            }
        });
    });
}

或者:

    // 等前面的任务执行完毕后,会回到主线程自动执行这个任务
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });

打印:

任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}

可以发现,先在子线程异步执行任务1和任务2,任务1和任务2都执行完成后,再回到主线程执行任务3。

如果有两个dispatch_group_notify呢?

    // 等前面的任务执行完毕后,会在异步线程,异步执行这两个任务
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3-%@", [NSThread currentThread]);
        }
    });

    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4-%@", [NSThread currentThread]);
        }
    });

打印:

任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}

可以发现,如果有两个dispatch_group_notify,任务1和任务2在子线程异步执行完成后,还会在子线程异步执行任务3和任务4。

总结:

使用GCD队列组,dispatch_group_notify(group, queue, ^{}) 函数会在group里面的任务执行完再执行,如果queue是主队列就在主线程执行这个函数的任务,如果queue是全局并发队列,就在子线程异步执行这个函数的任务。

Demo地址:GCD

你可能感兴趣的:(iOS-多线程1-GCD)