09 - OC多线程之队列任务以及死锁的认识

OC底层原理探索文档汇总

主要内容:

1、队列的认识
2、任务的认识
3、队列和任务搭配使用的面试题
4、死锁的认识和解决

队列

队列就是管理待执行任务的等待队列,用来调度任务给线程执行,符合先进先出原则

并发和串行决定了任务执行的顺序,并发是多个任务并发执行,串行是指任务顺序执行,也就是一个任务执行完成再执行下一个任务

  • 队列负责调度任务,提交任务给线程执行
  • 队列遵循先进先出原则,在这里指的是先进先调度。而不是先调度完成。
  • 队列底层会维护一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。
  • 串行队列底层只维护了一个线程,并发队列的底层维护了多个线程
  • 串行队列一次只能处理一个任务,上一个任务处理完成才能处理下一个任务
  • 并发队列可以同时处理多个任务,虽然处理顺序还是先进先出的原则,但是有的任务处理时间比较长,有可能先进后调度完成。
  • 队列是先进先调度,如果是串行队列是先进先调度结束,并发队列并不是先进先调度结束,可以同时调度多个任务

创建队列:

- (void)createQueueTest{
    //创建队列
    dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("串行队列", NULL);
    //获取已有队列
    //获取主队列
    dispatch_queue_t queue4 = dispatch_get_main_queue();
    //获取全局并发队列
    /*
     第一个参数是优先级,第二个无意义,填0
     */
    dispatch_queue_t queue5 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
}

任务

任务就是线程要执行的那段代码
执行任务有两种方式,同步和异步,同步和可以异步决定是否要开启新的线程,同步不开启,异步开启。

同步函数(sync):

  • 同步函数用来实现线程同步执行任务
  • 只能在当前线程中执行任务,不具备开启新线程的能力
  • 同步函数只能等block执行完成后才能返回

异步函数(async):

  • 异步函数用来实现线程异步执行任务
  • 异步函数不需要等block执行完成后就可以返回
  • 可以开启新线程

提交任务的函数很多都有两个版本,一个是传两个参数,传递作为任务的block代码,一个是传三个参数,传递作为任务的方法。

- (void)createTaskTest{
    /*
     参数一:队列
     参数二:作为任务的block代码
     */
     //在调度队列上提交异步执行的块并立即返回
    dispatch_async(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
    /*
     参数一:队列
     参数二:上下文
     参数三:作为任务的函数
     */
    dispatch_async_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
    
    /*
     参数一:队列
     参数二:作为任务的block代码
     */
     //提交块对象以执行,并在该块完成执行后返回。 
    dispatch_sync(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
    /*
     参数一:队列
     参数二:上下文
     参数三:作为任务的函数
     */
    dispatch_sync_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
}

注意:

  • 任务是线程执行的,不是队列执行,队列只是用来调度任务的
  • 任务的执行方式有同步和异步,同步不开辟新线程,异步会开辟新线程

主队列

主队列是一种特殊的串行队列,特殊在于只使用在主线程和主Runloop中,并且是在启动APP时自动创建的。
主队列是指主线程的队列,是一个串行队列,在主线程去执行其实就是放入了主队列

全局并发队列

全局并发队列是系统提供的,开发者可以直接使用的一个并发队列,没有其他的特殊之处。

在获取时可以给定优先级

  • 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

总结

任务队列线程的关系.png
  • 线程有两种方式执行任务,同步和异步,分别通过同步函数和异步函数来实现
  • 同步函数添加的任务,该线程执行完这个任务才可以执行其他任务,异步函数添加的任务,线程可以并发执行该任务
  • 任务存放在队列中,队列有两种调度方式,串行和并发,串行只能顺序处理任务,并发可以同时处理多个任务
  • 线程想要并发执行任务,需要异步函数添加任务给并发队列
  • 如果队列是串行的,即使线程可以并发执行任务也不行,因为队列是串行调度给线程的。
  • 同步函数需要等待block执行完成才可以返回
  • 异步函数不需要等待block执行完成就可以返回

队列和任务的搭配使用

任务和队列以及线程的执行关系.png

说明:

  • 同步不开启新线程,异步会开启新线程
  • 并发队列可以并发调度任务,串行队列只能顺序调度任务
  • 只有并发队列提交给线程异步执行的任务才可以异步执行

总结:

执行类型.png

队列和任务的常见类型代码演示

1、基础写法

给一个串行队列添加了一个任务,该任务需要异步执行

- (void)syncTest{
    // 把任务添加到队列 --> 函数
    // 任务 _t ref c对象
    dispatch_block_t block = ^{
        NSLog(@"hello GCD");
    };
    //串行队列
    dispatch_queue_t queue = dispatch_queue_create("wy", NULL);
    // 函数
    dispatch_async(queue, block);
    
}

2、主队列同步

- (void)mainSyncTest{
    
    NSLog(@"0");
    // 等
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });
    NSLog(@"2");
}

执行结果: 0,之后死锁

死锁.png

说明:

  • 在主队列中添加了三个任务,任务0,同步任务块,任务2
  • 同步任务块的任务本身就是给主队列添加了任务1
  • 因此现在主队列的任务有四个,并且顺序为任务0、同步任务块、任务2、任务1,这几个任务依次执行
  • 同步任务块需要等待任务1的完成才能返回,但是任务1的完成又在同步任务块的后面
  • 所以造成了同步任务块和任务1的相互等待,死锁导致程序崩溃

注意:造成死锁是同步任务块和任务1的相互等待,与任务2没关系,就算删掉任务2,也会死锁

3、 主队列异步

- (void)mainAsyncTest{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });
    NSLog(@"2");
}

执行结果: 21
说明:

  • 主队列中添加两个任务,异步任务块和任务2
  • 异步任务块的任务本身是给主队列添加了一个任务1
  • 这样主队列有三个任务,并且顺序为异步任务块、任务2、任务1
  • 因为任务1是异步提交的,也就是线程以异步的方式来执行,异步任务块不需要等block执行完成就可以返回
  • 因此这里不会发生死锁

4、 全局并发队列异步

并发执行任务

- (void)globalAsyncTest{
    
    for (int i = 0; i<20; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

结果:

2021-10-31 14:58:16.038522+0800 多线程使用[11062:5684222] hello queue
2021-10-31 14:58:16.038559+0800 多线程使用[11062:5684360] 0-<NSThread: 0x600003d438c0>{number = 6, name = (null)}
2021-10-31 14:58:16.038564+0800 多线程使用[11062:5684368] 2-<NSThread: 0x600003d34440>{number = 7, name = (null)}
2021-10-31 14:58:16.038567+0800 多线程使用[11062:5684359] 3-<NSThread: 0x600003d28780>{number = 4, name = (null)}
2021-10-31 14:58:16.038571+0800 多线程使用[11062:5684358] 1-<NSThread: 0x600003d282c0>{number = 8, name = (null)}
2021-10-31 14:58:16.038585+0800 多线程使用[11062:5684361] 4-<NSThread: 0x600003d31b80>{number = 3, name = (null)}
2021-10-31 14:58:16.038606+0800 多线程使用[11062:5684362] 5-<NSThread: 0x600003d43e00>{number = 5, name = (null)}
2021-10-31 14:58:16.038642+0800 多线程使用[11062:5684360] 7-<NSThread: 0x600003d438c0>{number = 6, name = (null)}

说明:

  • 异步执行会开启新线程,因此有多个线程来执行
  • 在并发队列中异步执行代码块,因此执行顺序是异步的
  • 并且打印hello是最早执行的,这是因为异步并发执行,异步添加任务消耗的时间比较多,所以先执行hello

5、 全局并发队列同步

串行执行任务

- (void)globalSyncTest{
    for (int i = 0; i<20; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");
}

结果:

2021-10-31 14:55:53.552472+0800 多线程使用[10971:5681912] 13-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.552541+0800 多线程使用[10971:5681912] 14-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.552635+0800 多线程使用[10971:5681912] 15-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.552729+0800 多线程使用[10971:5681912] 16-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.552829+0800 多线程使用[10971:5681912] 17-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.559220+0800 多线程使用[10971:5681912] 18-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.559332+0800 多线程使用[10971:5681912] 19-<_NSMainThread: 0x600000a2c7c0>{number = 1, name = main}
2021-10-31 14:55:53.559401+0800 多线程使用[10971:5681912] hello queue

说明:

  • 主队列中有同步任务块和hello两个任务,同步任务块添加了多个任务到并发队列中
  • 因为是同步函数,所以不会开启新线程,只有一个主线程来执行
  • 并且打印hello是在最后执行的,这是因为同步代码块是在hello任务的前面,同步函数需要等代码块执行完成后再返回,所以需要等同步代码块执行完才能执行hello

简单面试题分析

面试题1

- (void)textDemo1{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

}

结果: 15243

说明:

  • 在主队列中包含三个任务,同步任务1、异步任务代码块、同步任务5
  • 异步任务块不需要等待函数执行完成就可以返回,而异步函数本身是耗时的,所以前面的顺序是1、5
  • 在异步任务中将三个任务添加到一个并发队列中,包括任务2,同步代码块、任务4
  • 因为是并发队列异步提交任务,所以这三个任务并发执行,
  • 但因为队列的先进先出原则,先进的先调用,再看消耗的时间,同步代码块会消耗更多的时间,所以最后执行
  • 任务2和任务4执行时间一样,但是任务2先执行
  • 所以顺序为2、4、3,合起来就是15243

面试题2

- (void)textDemo{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
    NSLog(@"1");
    // 耗时
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

结果: 1、5、2,崩溃

20-面试题解析2.png

说明:

  • 在主队列中包含三个任务,同步任务1、异步任务代码块、同步任务5
  • 主队列是串行的,所以调度顺序是任务1、代码块、任务5
  • 异步任务块不需要等block执行完就可以返回,所以顺序是1、5
  • 代码块中将三个任务添加到一个并发队列中,包括任务2,同步代码块、任务4
  • 在串行队列中,包含任务2和同步任务块、任务4、任务3,而同步任务块的执行结束需要等待任务3的结束
  • 造成死锁,因此在执行完3后会崩溃

注意:
这里虽然是主队列,任务块在任务5的前面,但是在执行时我们看到先执行了任务5后执行了任务2,
这是因为异步操作并不需要等待函数的执行完成而完成,

面试题3

- (void)wbinterDemo{
    dispatch_queue_t queue11 = dispatch_queue_create("wy", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue11, ^{
        NSLog(@"1");
    });
    dispatch_async(queue11, ^{
        NSLog(@"2");
    });
    dispatch_sync(queue11, ^{
        NSLog(@"3");
    });
    NSLog(@"0");
    dispatch_async(queue11, ^{
        NSLog(@"1");
    });
    dispatch_async(queue11, ^{
        NSLog(@"7");
    });
    dispatch_async(queue11, ^{
        NSLog(@"8");
    });
    dispatch_async(queue11, ^{
        NSLog(@"9");
    }); 
}

请选择:
A:1230789
B:1237890
C:3120798
D:2137890

答案为AC

说明:

  • 首先这些任务块操作都是在主队列中,其中任务3不会开启新线程,和任务0都是主线程执行的,其他的任务在并发队列中,会开启新线程执行,同步任务块必须等任务3执行完才能继续往下执行,所以任务3肯定在任务0前面。(简单认识,同步并发是串行,所以任务3在任务0前面)
  • 7、8、9任务在任务0后添加,所以会在0任务后面执行
  • 同时7、8、9任务都是异步并发,所以并发执行,没有顺序
  • 任务1、2在任务3之前添加到并发队列中,也就是任务1和2与3在不同的队列中,而任务1和2又是异步并发的,所以他们三个会异步并发执行,他们三个是没有顺序的。

死锁的认识

死锁就是同一个串行队列中两个任务相互等待引起死锁.

网上面很多说是不同的线程相互等待引起的,是解释不通的,应该是队列,因为队列是先进先出原则导致,而后面进入的任务的结束又依赖于前面进的任务。
一个串行队列的线程池只维护了一个线程。

死锁的场景

主队列同步执行

- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务1");
});
}

说明:

  • 在主队列中有同步任务块,之后添加任务1到主队列中。
  • 而同步任务块的执行完成需要等block执行完成,所以同步任务块的执行依赖于任务1
  • 而在串行队列中,任务1的完成又需要同步任务块的完成。
  • 由此同步任务块和任务1相互等待,造成了死锁

同步串行队列的嵌套场景也是一样的,不再多言

- (void)textDemo{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
    NSLog(@"1");
    // 耗时
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

死锁的解决

上面知道死锁是一个串行队列中有两个任务相互等待导致的,所以有两个解决办法,

  • 队列为并发,不是串行
  • 异步任务,取消一方的等待,也就打破相互等待

所以可以有两个做法,

将队列改为并发

- (void)textDemo1{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

}

将任务改为异步任务,及不会相互等待

- (void)textDemo{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
    NSLog(@"1");
    // 耗时
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容