iOS底层系列23 -- 多线程的函数与队列

GCD

  • GCD全程为Grand Centeral Dispatch,翻译成中文为重要的中央调度方案;
  • 纯C语言实现是苹果公司为多核的并行运算提出的解决方案;
  • GCD会自动利用更多的CPU内核(比如双核、四核);
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  • GCD实现的就是:将任务添加到队列,并指定任务执行的函数。

GCD的使用

  • 通常我们使用GCD通常会这么写代码:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@" currentThread = %@",[NSThread currentThread]);
    });
  • 现在我们将上面的进行拆分:由任务 + 队列 + 函数三个部分构成
//队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//任务
dispatch_block_t taskBlock = ^{
   NSLog(@" currentThread = %@",[NSThread currentThread]);
};
//将任务添加到队列,并指定函数执行
dispatch_async(queue, taskBlock);
  • 使用dispatch_block_t 创建任务
  • 使用dispatch_queue_t 创建队列
  • 将任务添加到队列,并指定执行任务的函数dispatch_async

函数

dispatch_sync函数
  • 同步执行函数,会阻塞当前线程的继续执行
  • 必须等待队列中正在执行的任务执行完毕时,才会执行下一个任务;
  • 不具备开启新线程的能力,在当前线程中执行block任务;
  • 会立即去执行block任务;
dispatch_async函数
  • 异步执行函数,不会阻塞当前线程的继续执行;
  • 不用等待队列中正在执行的任务执行完毕,就可以执行下一个任务;
  • 具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关),则可能会开启新的线程执行block任务;
  • 不会立即去执行block任务,延迟执行;

队列

  • iOS多线程中所说的队列(Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取。每读取一个任务,则在队列中释放一个任务;
串行队列(serial queue)
  • 每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程(通俗理解:同一时刻只调度一个任务执行);
  • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL),创建串行队列
    其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示,这两种均表示默认的串行队列
dispatch_queue_t serial_one = dispatch_queue_create("com.lyy.queue_one", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t serial_two = dispatch_queue_create("com.lyy.queue.two", NULL);
并发队列(coucurrent queue)
  • 一次可以并发执行多个任务,即开启多个线程,并同时执行任务(通俗理解:同一时刻可以调度多个任务执行);
  • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT)创建并发队列;
  • 并发队列的并发功能只有在异步函数下才有效;
dispatch_queue_t coucurrent = dispatch_queue_create("com.lyy.queue_three", DISPATCH_QUEUE_CONCURRENT);
主队列(Main Dispatch Queue)
  • GCD中提供的特殊的串行队列;
  • 专门用来在主线程上调度任务的串行队列,依赖于主线程、主Runloop,在main函数调用之前自动创建;
  • 不会开启线程;
  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度;
  • 使用dispatch_get_main_queue()获得主队列;
  • 通常在返回主线程 更新UI时使用;
dispatch_queue_t mainQueue = dispatch_get_main_queue();
全局队列(Global Dispatch Queue)
  • GCD提供的默认的并发队列;
  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列;
  • 使用dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)
    • 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在iOS9之后,已经被服务质量(quality-of-service)取代;
    • 第二个参数使用0;
dispatch_queue_t global_queue = dispatch_get_global_queue(0, 0);

//优先级从高到低(对应的服务质量)依次为
- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND
全局队列与主队列的组合使用
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   //执行耗时操作
   dispatch_async(dispatch_get_main_queue(), ^{
        //回到主线程进行UI操作
    });
});

函数与队列的组合使用

【第一种:同步串行】
Snip20210322_1.png
  • 不会开辟新的线程,任务在当前主线程依次执行。
【第二种:异步串行】
Snip20210322_2.png
  • 只开辟一条新的子线程,任务在新的子线程上依次执行;
【第三种:同步并发】
Snip20210322_4.png
  • 由于同步不能开辟新的子线程,所以所有任务都在当前主线程上依次执行;
【第四种:异步并发】
Snip20210322_5.png
  • 因为异步能开辟子线程,至于开辟多少条子线程是由操作系统决定的,任务在多个子线程上无序执行;
【第五种:同步主队列】
Snip20210322_6.png
  • 同步主队列造成死锁,应用程序崩溃;

  • viewDidLoad函数在主线程中执行,执行到3的位置才会执行完毕;

  • 当执行到dispatch_sync函数时,向主队列中添加block任务,然后再取出block任务在主线程上执行且会阻塞主线程,当要执行block任务时,viewDidLoad已经在主线程中执行,必须等其执行完成才会执行block任务,而viewDidLoad要执行完成必须要等block任务执行完才能执行,造成相互等待,形成死锁;

  • 总结:

    • 主线程中的viewDidLoad与taskBlock两个任务相互等待,形成死锁;另一种解释就是当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度;
【第六种:异步主队列】
Snip20210322_8.png
  • 因为异步不会阻塞当前主线程,viewDidLoad立马执行完成;
  • 因为主队列的原因,不会开辟子线程,主队列中任务都在主线程上依次执行;
【第七种:同步全局队列】
Snip20210322_9.png
  • 同步-->主线程阻塞,主线程切换到全局队列上依次执行全局队列中的任务,全局队列中的任务执行完成再执行主线程中任务
【第八种:异步全局队列】
Snip20210322_10.png
  • 异步全局不阻塞当前线程,且开辟多条子线程执行全局队列中的任务,任务无序执行;

代码案例

案例一
- (void)viewDidLoad {
    [super viewDidLoad];
    //并行队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{//block1
        NSLog(@"2");
        dispatch_async(queue, ^{//block2
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
  • LLDB调试结果:
Snip20210322_11.png
  • 主线程的任务队列为:任务1、异步函数1、任务5,其中异步函数中的block1会延迟执行,任务1和任务5的任务复杂度是相同的,所以任务1和任务5优先于异步block1执行;
  • 在执行异步函数1时,往并发队列中追加任务block1(任务2,异步函数2,任务4),且开辟子线程1执行任务,其中异步函数2中的block2会延迟执行,任务2和任务4是复杂度一样,所以任务2和任务4优先于异步函数2执行;
  • 在执行异步函数2时,往并发队列中追加任务3,且开辟子线程2,然后子线程2执行任务3;
案例二
- (void)viewDidLoad {
    [super viewDidLoad];
    //并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    //异步函数
    dispatch_async(queue, ^{//block1
        NSLog(@"2");
        //同步函数
        dispatch_sync(queue, ^{//block2
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
  • LLDB调试结果:
Snip20210322_13.png
  • 主线程的任务队列为:任务1、异步函数、任务5,其中异步函数中的block1会延迟执行,任务1和任务5的任务复杂度是相同的,所以任务1和任务5优先于异步函数执行;
  • 在执行异步函数时,往并发队列中追加任务block1(包括任务2,同步函数与任务4),然后开辟子线程1执行任务block1,先执行任务2,
  • 接着遇到dispatch_sync同步,会阻塞当前子线程1的继续执行,且往并发队列中追加任务3,由于是在并发队列中,任务3无需等待block1执行完就可以执行(block1任务执行了一半,子线程切换执行任务3),当任务3执行完成时,当前子线程1会解除阻塞,开始执行任务4;
案例三
- (void)viewDidLoad {
    [super viewDidLoad];
    //串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);
    NSLog(@"1");
    //异步函数
    dispatch_async(queue, ^{//block1
        NSLog(@"2 --- %@",[NSThread currentThread]);
        //同步函数
        dispatch_sync(queue, ^{//block2
            NSLog(@"3 --- %@",[NSThread currentThread]);
        });
        NSLog(@"4 --- %@",[NSThread currentThread]);
    });
    NSLog(@"5");
}
  • LLDB调试结果:
Snip20210323_21.png
  • 主线程的任务队列为:任务1、异步函数、任务5,其中异步函数中的block1会延迟执行,任务1和任务5的任务复杂度是相同的,所以任务1和任务5优先于异步函数执行;
  • 在执行异步函数时,往串行队列中追加任务block1(包括任务2,同步函数与任务4),然后开辟子线程1执行任务block1,先执行任务2;
  • 接着遇到dispatch_sync同步,会阻塞当前子线程1的继续执行,且往串行队列中追加任务3,由于是在串行队列中,block1已经在执行了但没有执行完成,所以必须要等block1执行完成才会执行任务3,即任务3等待block1; 而block1要执行完成(即任务4执行完),必须任务3执行完成解除当前子线程1阻塞才会执行任务4,即block1等待任务3;任务3与block1相互等待,形成死锁;
案例四
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_async(dispatch_get_global_queue(0,0),^{//block1
        NSLog(@"2");
        dispatch_sync(dispatch_get_main_queue(),^{//block2
            NSLog(@"3 -- %@",[NSThread currentThread]);
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
  • LLDB调试结果:
Snip20210323_22.png
  • 主线程的任务队列为:任务1、异步函数、任务5,其中异步函数中的block1会延迟执行,任务1和任务5的任务复杂度是相同的,所以任务1和任务5优先于异步函数执行;
  • 在执行异步函数时,往全局并发队列中追加任务block1(包括任务2,同步函数与任务4),然后开辟子线程1执行任务block1,先执行任务2;
  • 接着遇到dispatch_sync同步,会阻塞当前子线程1的继续执行,且往主队列中追加任务3,当前主线程没有阻塞可以正常执行任务3,当任务3在主线成中执行完成时,当前子线程1会解除阻塞,接着执行任务4;
案例五
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(dispatch_get_global_queue(0,0),^{//block1
        NSLog(@"1");
        dispatch_sync(dispatch_get_main_queue(),^{//block2
            NSLog(@"2");
        });
        NSLog(@"3");
    });
    NSLog(@"4");
    while(1){

    }
    NSLog(@"5");
}
  • LLDB调试结果:
Snip20210323_23.png
  • 主线程队列中追加任务:异步函数,任务4,死循环,任务5,其中异步函数中的block1会延迟执行,所以任务4率先执行,遇到死循环,出现主线程阻塞,任务5永远执行不了;
  • 在执行异步函数时,往全局并发队列中追加block1任务(任务1,同步函数,任务3),然后开辟子线程1,执行开始执行block1,首先执行任务2;
  • 接着遇到同步函数,会阻塞当前子线程1且往主队列中追加任务2,由于主线程死循环阻塞了,任务2无法执行,导致子线程1一直阻塞,任务3也无法执行;
案例六
- (void)viewDidLoad {
    [super viewDidLoad];
    //并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", 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");
    });
}
  • 执行结果:0一定在3之后,且在789之前;

你可能感兴趣的:(iOS底层系列23 -- 多线程的函数与队列)