iOS原理 多线程2 -- GCD详解

东篱采桑人IP属地: 广东
字数 4,692阅读 509

iOS原理 文章汇总

一、GCD简介

GCD全称是Grand Central Dispatch,是Apple开发中用于实现多线程编程的一种解决方案。GCD是纯C语言,提供了很多强大的函数。

GCD的优势如下:

  • GCD 可用于多核的并行运算;
  • GCD 会自动利用更多的CPU内核(比如双核、四核);
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  • 代码简洁,不需要编写任何线程管理的代码。

二、GCD的任务和队列

GCD中有两个核心概念:『任务』和『队列』。

1.任务

任务即执行操作,简单来说是在线程上执行的那段代码,在GCD中被封装在block中。任务的执行方式有两种:『同步执行』,『异步执行』。

同步执行sync

  • 不会开启新线程,只在当前线程中串行执行任务,需要等待。
  • 对于代码执行来说,必须等待当前语句执⾏完毕,才会执⾏下⼀条语句。

异步执行async

  • 会开启新线程,并发执行任务,不需等待。
  • 对于代码执行来说,不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句。
2.队列(Dispatch Queue)

这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即最早被添加的任务会先被调度执行。每执行一个任务,就从队列中释放一个任务。队列的结构可参考下图:

GCD中的队列也分为两种:『串行队列』,『并发队列』。

  • 串行队列Serial Dispatch Queue):只开启一个线程,同一时间只执行一个任务,一个任务执行完毕后,再执行下一个任务。
  • 并发队列Concurrent Dispatch Queue):可以开启多个线程,同一时间可以并发执行多个任务,不用等待。

注意:并发队列中的任务只有在异步执行下才会开启多个线程,同步执行下只会在当前线程中处理任务。

两者的任务执行情况如下图所示:

串行队列(Serial Dispatch Queue)
并发队列(Concurrent Dispatch Queue)

三、GCD的使用步骤

GCD使用步骤教简单,只有两步:
1. 创建一个队列(串行队列或者并行队列)。
2. 创建任务,将任务添加到队列(创建时需指定是同步执行还是异步执行)。

1.队列的创建和获取
  • 创建串行队列和并发队列

    通过dispatch_queue_create函数可以创建串行队列和并发队列,函数需要传入两个参数:

    • 参数1:为队列的标识符,可以为空;
    • 参数2:为队列的类型,DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。其中DISPATCH_QUEUE_SERIAL本质是NULL
    //创建一个串行队列,参数2也可以直接传NULL
    dispatch_queue_t sQueue = dispatch_queue_create("lab1", DISPATCH_QUEUE_SERIAL);
    //创建一个并发队列
    dispatch_queue_t cQueue = dispatch_queue_create("lab2", DISPATCH_QUEUE_CONCURRENT);
    
    #define DISPATCH_QUEUE_SERIAL NULL  
    
  • 获取主队列(Main Dispatch Queue)

    主队列是一种特殊的串行队列,程序启动时由系统默认提供,所有任务均默认添加在主队列。主队列中所有的任务,无论是同步执行还是异步执行,都只会在主线程中执行。通过dispatch_get_main_queue()函数来获取主队列:

    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
  • 获取全局并发队列(Global Dispatch Queue)

    在使⽤多线程开发时,如果对队列没有特殊需求,在执⾏异步任务时,可以直接使⽤全局并发队列。通过dispatch_get_global_queue函数来获取全局并发队列,需要传入两个参数:

    • 参数1:表示队列优先级,一般传入 DISPATCH_QUEUE_PRIORITY_DEFAULT
    • 参数2:暂时没用,传0即可。
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
2.任务的创建和添加

GCD 提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async

//同步执行任务创建方法
dispatch_sync(queue, ^{
    //这里放同步执行任务代码
});

//异步执行任务创建方法
dispatch_async(queue, ^{
    //这里放异步执行任务代码
});

四、对串行队列的分析

串行队列里的任务,会遵循FIFO规则依次调度执行,由于任务的执行方式分为同步执行和异步执行,所以串行队列中的任务存在以下的情况:

  1. 全部都是同步执行的任务
  2. 全部都是异步执行的任务
  3. 既有同步执行又有异步执行的任务
  4. 线程死锁

接下来通过具体示例来分析这几种组合情况。

1. 全部都是同步执行的任务

如下面代码所示,往一个串行队列中添加三个同步执行任务,然后打印执行结果。

//串行队列+同步执行任务
-(void)testSerialAndSync{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    //添加同步执行任务1
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务2
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务3
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-15 01:16:07.499128+0800 GCDDemo[17257:70356957]  ==== method thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:07.499199+0800 GCDDemo[17257:70356957]  ==== begin
2020-11-15 01:16:09.500480+0800 GCDDemo[17257:70356957]  ==== task1 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:11.501599+0800 GCDDemo[17257:70356957]  ==== task2 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:13.502912+0800 GCDDemo[17257:70356957]  ==== task3 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:13.503060+0800 GCDDemo[17257:70356957]  ==== end

从打印结果可知,三个任务均在当前线程(主线程)里按顺序依次执行。因此,当一个串行队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行

2. 全部都是异步执行的任务

如下面代码所示,往一个串行队列中添加三个异步执行任务,然后打印执行结果。

//串行队列+异步执行任务
-(void)testSerialAndAsync{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    //添加异步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:3];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加异步执行任务2
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加异步执行任务3
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:1];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 10:12:25.065872+0800 GCDDemo[41263:73331602]  ==== method thread = <NSThread: 0x60000170c100>{number = 1, name = main}
2020-11-16 10:12:25.065953+0800 GCDDemo[41263:73331602]  ==== begin
2020-11-16 10:12:25.066017+0800 GCDDemo[41263:73331602]  ==== end
2020-11-16 10:12:28.071048+0800 GCDDemo[41263:73331984]  ==== task1 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}
2020-11-16 10:12:30.074008+0800 GCDDemo[41263:73331984]  ==== task2 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}
2020-11-16 10:12:31.076458+0800 GCDDemo[41263:73331984]  ==== task3 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}

从打印结果可知,这个过程中只新开启了一条子线程,三个任务在子线程中按序依次执行。因此,当一个串行队列里全是异步执行的任务,只会开启一个新线程,所有任务在这个线程里按顺序同步执行

3. 既有同步执行又有异步执行的任务

如下面代码所示,往一个串行队列中穿插添加两个同步执行的任务和两个异步执行的任务。

//串行队列+两种方式执行的任务
-(void)testSerial{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    //添加异步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:4];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务2
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:3];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加异步执行任务3
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务4
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:1];
        NSLog(@" ==== task4 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 14:25:03.502639+0800 GCDDemo[56034:75334852]  ==== method thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:03.502730+0800 GCDDemo[56034:75334852]  ==== begin
2020-11-16 14:25:07.503842+0800 GCDDemo[56034:75335420]  ==== task1 thread = <NSThread: 0x600001749a80>{number = 2, name = (null)}
2020-11-16 14:25:10.505354+0800 GCDDemo[56034:75334852]  ==== task2 thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:12.506879+0800 GCDDemo[56034:75335420]  ==== task3 thread = <NSThread: 0x600001749a80>{number = 2, name = (null)}
2020-11-16 14:25:13.508375+0800 GCDDemo[56034:75334852]  ==== task4 thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:13.508554+0800 GCDDemo[56034:75334852]  ==== end

从打印结果可知,这个过程中新开启了一条子线程,两个异步任务在子线程执行,两个同步任务在当前线程(主线程)中执行,且这4个任务是按顺序依次执行。因此,当一个串行队列中既有同步执行任务,还有异步执行任务时,会新开启一个子线程,所有异步任务均在子线程上执行,所有同步任务均在当前线程中执行,虽然是在两个线程上执行,但所有任务依旧按序同步执行

4. 线程死锁

当往一个串行队列中添加同步执行任务时,若使用不当,可能会导致线程死锁,这里对普通串行队列和主队列来分别进行分析。

  • 普通串行队列死锁
-(void)testSerialAndSyncLock{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    //添加异步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
        //添加同步执行任务2
        dispatch_sync(queue, ^{
            
            [NSThread sleepForTimeInterval:2];
            NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
        });
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 14:48:47.797169+0800 GCDDemo[57469:75514015]  ==== method thread = <NSThread: 0x600001708640>{number = 1, name = main}
2020-11-16 14:48:47.797242+0800 GCDDemo[57469:75514015]  ==== begin
2020-11-16 14:48:47.797304+0800 GCDDemo[57469:75514015]  ==== end
2020-11-16 14:48:49.802076+0800 GCDDemo[57469:75514581]  ==== task1 thread = <NSThread: 0x600001757dc0>{number = 2, name = (null)}

//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow

在这个例子中,先往串行队列queue中添加一个异步执行任务task1,然后在task1里又往队列中添加同步执行任务task2。从前面的分析可知,task1会在子线程中执行,task2会在主线程中执行,但从打印结果可知,子线程死锁了。

为什么会这样呢?从代码逻辑可知,串行队列queue里的任务情况如下图所示:

由于是串行队列,所以需要先执行完task1,才能执行task2。上文已说明,任务其实是写在block里的代码,所以task1里具体包含sleepForTimeIntervalNSLogdispatch_sync这三个操作,而执行dispatch_sync操作,必须要先执行其block里的任务(也就是task2),所以这里完成task1的执行需要先等待task2执行完,而task2的执行又必须等待task1的完成,这样就造成了两个任务互相等待的局面,导致串行队列阻塞,使得正在执行任务的子线程死锁。

  • 主队列死锁
-(void)testMainQueueAndSyncLock{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    //添加同步执行任务1
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    
    
    NSLog(@" ==== end");
}

//执行结果
2020-11-16 15:10:25.390210+0800 GCDDemo[58693:75674415]  ==== method thread = <NSThread: 0x600001709040>{number = 1, name = main}
2020-11-16 15:10:25.390282+0800 GCDDemo[58693:75674415]  ==== begin

//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow

这里主线程死锁的原因和上面一样,虽然说task1里没有添加其他同步执行任务,但是外层方法testMainQueueAndSyncLock本身就是添加在主队列里的任务,方法里包含了dispatch_sync操作,所以需要先执行block里的任务(即task1)。而这个dispatch_sync操作是将task1添加到主队列中,所以又需要先等外层方法testMainQueueAndSyncLock执行完才能执行task1,这样就造成外层方法和task1互相等待,阻塞了主队列,导致主线程死锁。

  • 主队列+同步执行不一定会死锁

这里要特别说明一下,之前有看过其他文章,说主队列+同步执行一定会造成死锁,其实是不准确的,只是因为“执行主队列+同步执行的任务”本身也处在主队列中同步执行,所以会导致主线程死锁。来看下面的例子:

-(void)testMainQueueAndSyncLock{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("lab", DISPATCH_QUEUE_SERIAL);
    //添加异步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
        //添加同步执行任务2到主队列中
        dispatch_sync(dispatch_get_main_queue(), ^{
            
            [NSThread sleepForTimeInterval:2];
            NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
        });
    });

   //这里延迟8s
   [NSThread sleepForTimeInterval:8];
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 15:22:38.153895+0800 GCDDemo[59402:75766119]  ==== method thread = <NSThread: 0x600001708280>{number = 1, name = main}
2020-11-16 15:22:38.153978+0800 GCDDemo[59402:75766119]  ==== begin
2020-11-16 15:22:40.156534+0800 GCDDemo[59402:75766421]  ==== task1 thread = <NSThread: 0x60000174b440>{number = 2, name = (null)}
2020-11-16 15:22:46.154269+0800 GCDDemo[59402:75766119]  ==== end
2020-11-16 15:22:48.247564+0800 GCDDemo[59402:75766119]  ==== task2 thread = <NSThread: 0x600001708280>{number = 1, name = main}

这里先往一个串行队列queue中添加一个异步执行任务task1,再在tsak1里往主队列中添加同步执行任务task2,从执行结果可知,并未死锁。通过代码逻辑可知,在这个过程中存在两个队列:主队列和串行队列queue。串行队列queue中只有一个异步执行的任务task1,不会造成死锁,所以主要看主队列中的情况。

主队列里存在两个同步执行任务,会先执行任务外层方法testMainQueueAndSyncLock,再执行task2。外层方法里包含了NSLogsleepForTimeIntervaldispatch_async这几个操作,而执行dispatch_async的操作不需要先将其block里的任务执行完,所以可以直接将外层方法执行完,然后再执行另一个任务task2,这样两个任务不会互相等待,也就不会导致主线程死锁。

5. 串行队列分析后的结论

根据上面的分析可知,串行队列在各场景下的使用情况如下:

  1. 当一个串行队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
  2. 当一个串行队列里全是异步执行的任务,只会开启一个新线程,所有任务在这个线程里按顺序同步执行。
  3. 当一个串行队列中既有同步执行任务,还有异步执行任务时,会新开启一个子线程,所有异步任务均在子线程上执行,所有同步任务均在当前线程中执行,虽然是在两个线程上执行,但所有任务依旧按序同步执行。
  4. 若在某个串行队列中使用当前串行队列+同步执行任务,则会阻塞当前队列,导致当前线程死锁。

五、对并发队列的分析

并发队列也遵循FIFO原则,和串行队列一样,也会存在以下几种情况:

  1. 全部都是同步执行的任务
  2. 全部都是同步执行的任务
  3. 既有同步执行又有异步执行的任务

接下来也通过具体示例来分析这几种组合情况。

1. 全部都是同步执行的任务

如下面代码所示,往一个并发队列中添加三个同步执行任务,然后打印执行结果。

//并发队列+同步执行任务
-(void)testConcurrentAndSync{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    //添加同步执行任务1
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:3];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务2
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务3
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:1];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

2020-11-16 16:03:40.559502+0800 GCDDemo[61734:76074945]  ==== method thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:40.559576+0800 GCDDemo[61734:76074945]  ==== begin
2020-11-16 16:03:43.559757+0800 GCDDemo[61734:76074945]  ==== task1 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:45.560417+0800 GCDDemo[61734:76074945]  ==== task2 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:46.560727+0800 GCDDemo[61734:76074945]  ==== task3 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:46.560801+0800 GCDDemo[61734:76074945]  ==== end

从打印结果可知,这三个任务均在主线程中按序执行。因此,当一个并发队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行

2. 全部都是异步执行的任务

如下面代码所示,往一个并发队列中添加三个异步执行任务,然后打印执行结果。

//并发队列+异步执行任务
-(void)testConcurrentAndAsync{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    //添加同步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:3];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务2
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务3
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:1];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 16:08:30.368449+0800 GCDDemo[62020:76112767]  ==== method thread = <NSThread: 0x600001708600>{number = 1, name = main}
2020-11-16 16:08:30.368531+0800 GCDDemo[62020:76112767]  ==== begin
2020-11-16 16:08:30.368594+0800 GCDDemo[62020:76112767]  ==== end
2020-11-16 16:08:31.369037+0800 GCDDemo[62020:76113117]  ==== task3 thread = <NSThread: 0x60000176a980>{number = 2, name = (null)}
2020-11-16 16:08:32.372892+0800 GCDDemo[62020:76113114]  ==== task2 thread = <NSThread: 0x60000176c2c0>{number = 3, name = (null)}
2020-11-16 16:08:33.373147+0800 GCDDemo[62020:76113116]  ==== task1 thread = <NSThread: 0x600001767340>{number = 4, name = (null)}

从打印结果可知,三个任务在三条不同的线程上异步执行。因此,当一个并发队列里都是异步执行任务时,会给每一个任务开启一条新线程,来异步执行这些任务

3. 既有同步执行又有异步执行的任务

如下面代码所示,往一个并发队列中穿插添加两个同步执行的任务和两个异步执行的任务。

//并发队列+同步和异步执行任务
-(void)testConcurrent{
    
    NSLog(@" ==== method thread = %@", [NSThread currentThread]);
    NSLog(@" ==== begin");
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    //添加异步执行任务1
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:4];
        NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务2
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:3];
        NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
    });
    //添加异步执行任务3
    dispatch_async(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
    });
    //添加同步执行任务4
    dispatch_sync(queue, ^{
        
        [NSThread sleepForTimeInterval:2];
        NSLog(@" ==== task4 thread = %@", [NSThread currentThread]);
    });
    
    NSLog(@" ==== end");
}

//打印结果
2020-11-16 16:32:19.942009+0800 GCDDemo[63414:76291148]  ==== method thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:19.942079+0800 GCDDemo[63414:76291148]  ==== begin
2020-11-16 16:32:22.942793+0800 GCDDemo[63414:76291148]  ==== task2 thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:23.944156+0800 GCDDemo[63414:76291650]  ==== task1 thread = <NSThread: 0x600001742b00>{number = 2, name = (null)}
2020-11-16 16:32:24.943348+0800 GCDDemo[63414:76291148]  ==== task4 thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:24.943353+0800 GCDDemo[63414:76291576]  ==== task3 thread = <NSThread: 0x600001742b80>{number = 3, name = (null)}
2020-11-16 16:32:24.943470+0800 GCDDemo[63414:76291148]  ==== end

从打印结果可知,两个同步任务均在主线程中按序执行,两个异步任务在两个不同的子线程中执行(打印结果的顺序后面再分析)。因此,当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行

4. 并发队列分析后的结论

根据上面的分析可知,串行队列在各场景下的使用情况如下:

  1. 当一个并发队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
  2. 当一个并发队列里都是异步执行任务时,会给每一个任务开启一条新线程,来异步执行这些任务
  3. 当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行。

六、同步函数和异步函数的耗时情况

对相同复杂度的任务来说,dispacth_syncdispatch_async执行时间是不一样的。

  • dispacth_async
-(void)testAsync{
    
    CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{

        NSLog(@" ==== async interval = %f", CFAbsoluteTimeGetCurrent() - time);
    });
    
    NSLog(@" ==== interval = %f", CFAbsoluteTimeGetCurrent() - time);
}

//打印结果
2020-11-16 19:05:31.131300+0800 GCDDemo[72585:77510232]  ==== interval = 0.000019
2020-11-16 19:05:31.131308+0800 GCDDemo[72585:77510854]  ==== async interval = 0.000029

从打印结果可以看出,dispatch_async函数虽然先执行,但执行相同复杂度任务所需的时间,还是比直接执行要久。

  • dispacth_async和dispatch_sync
-(void)testAsyncAndSync{
    
    CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{

        NSLog(@" ==== async interval = %f", CFAbsoluteTimeGetCurrent() - time);
    });

    dispatch_sync(queue, ^{

        NSLog(@" ==== sync interval = %f", CFAbsoluteTimeGetCurrent() - time);
    });
}

//打印结果
2020-11-16 19:09:08.919239+0800 GCDDemo[72810:77540305]  ==== sync interval = 0.000033
2020-11-16 19:09:08.919398+0800 GCDDemo[72810:77540590]  ==== async interval = 0.000214

从打印结果可知,执行相同复杂度的任务时,dispatch_async要比dispatch_sync所需时间更长。

六、面试题解析

学习了前面的知识后,接下来对几个高频次面试题的输出结果进行解析。

1. 并发队列 + 异步执行
-(void)interview1{
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@" ==== 1");
    dispatch_async(queue, ^{
       
        NSLog(@" ==== 2");
        dispatch_async(queue, ^{
           
            NSLog(@" ==== 3");
        });
        NSLog(@" ==== 4");
    });
    NSLog(@" ==== 5");
}

//打印结果
2020-11-16 19:17:51.449472+0800 GCDDemo[73315:77606414]  ==== 1
2020-11-16 19:17:51.449562+0800 GCDDemo[73315:77606414]  ==== 5
2020-11-16 19:17:51.449575+0800 GCDDemo[73315:77606769]  ==== 2
2020-11-16 19:17:51.449634+0800 GCDDemo[73315:77606769]  ==== 4
2020-11-16 19:17:51.449646+0800 GCDDemo[73315:77606768]  ==== 3

这道题比较简单,分析思路:先整体,再局部。

  • 首先,从整体上看,外层方法interview1是在主线程中执行,里面的操作按顺序执行,主要包含:NSLog(@" ==== 1")dispatch_asyncNSLog(@" ==== 5")。由于dispatch_async是异步执行无需等待,且比直接执行形同复杂度的任务耗时更久,所以会先输出1,5,再输出dispatch_async函数block里的打印。
  • 再看dispatch_async里的任务,里面包含三个操作:NSLog(@" ==== 2")dispatch_asyncNSLog(@" ==== 4")。这三个操作在新开启的子线程里按顺序执行,和interview1里的操作一样,输出顺序为:2、4、3。

因此,经过分析得知,这道题的输出顺序为:1、5、2、4、3。

这里将并发队列改为串行队列,输出顺序还是一样,大家可以自己分析下。

2. 并发队列 + 异步函数嵌套同步函数
-(void)interview2{
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@" ==== 1");
    dispatch_async(queue, ^{
       
        NSLog(@" ==== 2");
        dispatch_sync(queue, ^{
           
            NSLog(@" ==== 3");
        });
        NSLog(@" ==== 4");
    });
    NSLog(@" ==== 5");
}

//打印结果
2020-11-16 19:44:38.915026+0800 GCDDemo[74906:77810083]  ==== 1
2020-11-16 19:44:38.915117+0800 GCDDemo[74906:77810083]  ==== 5
2020-11-16 19:44:38.915127+0800 GCDDemo[74906:77810255]  ==== 2
2020-11-16 19:44:38.915168+0800 GCDDemo[74906:77810255]  ==== 3
2020-11-16 19:44:38.915201+0800 GCDDemo[74906:77810255]  ==== 4

这道题只是在上一题上做个小改动,将里面的dispatch_async改为dispatch_sync。所以分析思路还是一样:先整体,再局部。

  • 从整体上看,还是先输出1、5,再输出dispatch_async函数block里的任务。
  • dispatch_async里的任务来看,三个操作NSLog(@" ==== 2")dispatch_sync以及NSLog(@" ==== 4")在新开的子线程中同步执行,所以先输出2,而dispatch_sync是同步函数,所以需要等待其block里的任务执行完,才能执行后面的任务,所以先输出3,再输出4。

因此,经过分析得知,这道题的输出顺序为:1、5、2、3、4。

3. 串行队列 + 异步函数嵌套同步函数
-(void)interview3{
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    NSLog(@" ==== 1");
    dispatch_async(queue, ^{
       
        NSLog(@" ==== 2");
        dispatch_sync(queue, ^{
           
            NSLog(@" ==== 3");
        });
        NSLog(@" ==== 4");
    });
    NSLog(@" ==== 5");
}

//打印结果
2020-11-16 20:06:27.263232+0800 GCDDemo[76170:77980426]  ==== 1
2020-11-16 20:06:27.263325+0800 GCDDemo[76170:77980426]  ==== 5
2020-11-16 20:06:27.263338+0800 GCDDemo[76170:77980465]  ==== 2

//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow

这里输出1、5、2后就报错,因为任务互相等待,阻塞了串行队列queue,导致子线程死锁了(具体分析查在上文的『普通串行队列死锁』)。

4. 并发队列 + 异步函数嵌套同步函数
-(void)interview4{
    
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        
        NSLog(@" ==== 1");
    });
    dispatch_async(queue, ^{
        
        NSLog(@" ==== 2");
    });
    dispatch_sync(queue, ^{
        
        NSLog(@" ==== 3");
    });
    NSLog(@" ==== 0");
    dispatch_async(queue, ^{
        
        NSLog(@" ==== 7");
    });
    dispatch_async(queue, ^{
        
        NSLog(@" ==== 8");
    });
    dispatch_async(queue, ^{
        
        NSLog(@" ==== 9");
    });
}

//打印结果
2020-11-16 21:36:24.898847+0800 GCDDemo[81805:78779526]  ==== 3
2020-11-16 21:36:24.898907+0800 GCDDemo[81805:78779885]  ==== 2
2020-11-16 21:36:24.898880+0800 GCDDemo[81805:78779883]  ==== 1
2020-11-16 21:36:24.898930+0800 GCDDemo[81805:78779526]  ==== 0
2020-11-16 21:36:24.898997+0800 GCDDemo[81805:78779883]  ==== 7
2020-11-16 21:36:24.899024+0800 GCDDemo[81805:78779885]  ==== 8
2020-11-16 21:36:24.899045+0800 GCDDemo[81805:78779883]  ==== 9

这里的输出结果只是其中的一种情况,还有其他情况。从前面对并发队列分析后的结论可知,当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行。因此这题的分析思路:先同步,再异步。

  • 30的输出是处于外层方法interview4中的同步执行任务,所以0必定会在3后输出
  • 789三个输出任务的dispatch_async操作是在外层方法interview4中同步执行的,且在同步任务NSLog(@" ==== 0")后面,所以7、8、9必定在0后输出
  • 12789这五个输出任务均在并发队列queue中,且会在不同的子线程上异步执行,不需要等待,所以它们的执行不分先后

因此,经过分析可知,0必定在3的后面输出,7、8、9必定在0的后面输出,而根据任务的复杂度不同,1,2在任何位置出现都有可能。

推荐阅读

1. iOS原理 多线程1 -- 多线程的作用和原理
2. iOS 多线程:『GCD』详尽总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
0人点赞
更多精彩内容,就在简书APP
"小礼物走一走,来简书关注我"
还没有人赞赏,支持一下
东篱采桑人采桑东篱下,悠然看世界。努力成为你想要的自己,加油!
总资产6共写了6.4W字获得85个赞共67个粉丝
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,578评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,701评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,691评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,974评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,694评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,026评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,015评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,193评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,719评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,442评论 3 360
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,668评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,151评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,846评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,255评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,592评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,394评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,635评论 2 380

推荐阅读更多精彩内容