IOS面试题(多线程) --- GCD

OC面试题目合集地址

关键词英文简译:

dispatch: 派遣 / 派出 / 发出 / 发送
sync: 同步, 同时
async:  异步, 不同时
queue: 队列

问题1: GCD调用的四种组合/ 讲一下你对GCD的了解

  • 同步串行: 同步分配任务到串行队列
dispatch_sync(serial_queue, ^{ 任务 })
  • 异步串行: 异步分配任务到串行队列
dispatch_async(serial_queue, ^{ 任务 })
  • 同步并行: 同步分配任务到并发队列
dispatch_sync(concurrent_queue, ^{ 任务 })
  • 异步并行: 异步分配任务到并发队列
dispatch_async(concurrent_queue, ^{ 任务 })

同步函数不具备开启线程的能力,无论是什么队列都不会开启线程;异步函数具备开启线程的能力,开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)

同步函数 dispatch_sync

  • 串行队列: 不会开启线程
  • 并发队列: 不会开启线程

异步函数 dispatch_async

  • 串行队列: 1条线程
  • 并发队列: N条线程

留意下, 异步有不新开线程情况

任务追加线程已存在,比如 main 线程,则不会开线程,而是使用线程。例如: 在串行队列Queue任务A里异步将任务B追加到队列Queue中,此时任务B任务A在一个线程,不开线程。



问题2: GCD自定义queue有几种类型

serial串行

又称为private dispatch queues,同一时刻只执行一个任务,并按添加到serial的顺序执行。当创建多个serial queue时,虽然它们各自是同步执行的,但serial queueserial queue之间是并发执行的。serial queue通常用于同步访问特定的资源或数据。

concurrent并行

又称为global dispatch queue,同一时刻可执行多个任务,任务开始执行的顺序按添加的顺序执行,但是执行完成的顺序是随机的,同时可以创建执行的任务数量依赖系统条件。

Main dispatch queue 主队列

全局可用的serial queue,它是在应用程序主线程上执行任务的。

主队列

看下官方注释:

主队列的并不完全像常规串行队列,
当在非UI应用程序的进程中使用时,它可能会产生不必要的副作用
(守护程序)对于此类进程,应避免使用主队列。
调用main() 之前的主线程会自动创建主队列

验证main之前就有主线程&主队列

所以主队列其实是一种特殊的串行队列, 题目只回答 串行&并行 也是对的
dispatch_get_global_queue 全局队列, 是并行队列



问题3: 下面代码运行结果会什么样?

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSLog(@"1");
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        
        NSLog(@"2");
        
    });
    
    NSLog(@"3");
}

考察同步串行问题

答案:

先打印 1 然后报错 Crash

其实因为这里的dispatch_sync是个死锁

    // 死锁
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
  • 死锁原因: 由队列引起的循环等待 (留意下并不是由线程引起的循环等待)

这段代码的进行的逻辑为


问题3逻辑
  • 我们首先在主队列里面提交了一个viewDidLoad任务, 接下来又提交了一个Block任务。当然这两个任务都被分配到主线程中去执行。

  • 现在我们分配viewDidLoad到主线程中去处理, 因为viewDidLoad中有dispatch_get_main_queue 的 Block, 那么只有Block调用之后viewDidLoad才能往下走。

  • 而主队列有个特点: 先进先出 FIFO, 即block执行完成必须要等待viewDidLoad结束(完成)才可以。这样就形成了一个相互等待的场面 --- 死锁

验证:

问题3结果


问题4: 下面代码运行结果会什么样?

    NSLog(@"1");

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

    dispatch_sync(queue, ^{

        NSLog(@"2");

    });

    NSLog(@"3");

跟问题3不同, 这端代码涉及2个队列, 主队列和自定义串行队列queue

问题4图解
  • 主队列提交一个viewdidload方法, 当然这个方法在主线程里面执行
  • viewdidload执行中, 需要同步提交一个任务到自定义串行队列上面, 同步提交需要在相同线程执行, 固也在主线程执行
  • 串行队列在主线程执行任务之后, 再去继续执行主队列后续代码逻辑

所以正常运行

答案:

正常运行 1 2 3

问题4答案

当然如果创建的队列是主队列, 如下

    // dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue = dispatch_get_main_queue();

那么问题4例子还是跟问题3一样, 死锁 crash



问题5: 下面代码运行结果会什么样?

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    NSLog(@"1");
    
    dispatch_sync(queue, ^{
        
        NSLog(@"2");
        
        dispatch_sync(queue, ^{
            
            NSLog(@"3");
            
        });
        
        NSLog(@"4");
        
    });
    
    NSLog(@"5");
  
}

  • 同步提交任务无论串行或者并发都是在当前线程中执行。上面例子在viewDidLoad里面执行, 即主线程里面执行。
  • 先执行NSLog(@"1")然后走同步并发队列dispatch_sync(queue, ^{, 里面的NSLog(@"2")
  • 里层的dispatch_sync(queue, ^{,也是个串行并发队列, 在当前线程执行即主线程
  • 同时由于并发队列特点, 我们提交到这个队列里面的block可以并发执行, 那么继续执行NSLog(@"3")
  • 之后继续执行NSLog(@"4"), NSLog(@"5")

答案:

正常运行 1 2 3 4 5

问题5结果


问题6: 下面代码运行结果会什么样?

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("test1", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"1");

    dispatch_sync(queue, ^{

        NSLog(@"2");

        dispatch_sync(queue, ^{

            NSLog(@"3");

        });

        NSLog(@"4");

    });

    NSLog(@"5");

}

其实这个道题跟问题3 道理相同

原因都是: 由队列引起的循环等待造成 死锁

  • 同步提交任务无论串行或者并发都是在当前线程中执行。上面例子在viewDidLoad里面执行, 即主线程里面执行。

  • 里层外层的串行队列都是相同线程, 即主线程中执行。由于是同步方式外层dispatch_sync结束需要等待里层dispatch_sync结束, 而里层dispatch_sync结束有需要等待外层dispatch_sync结束, 这样形成循环等待导致死锁

答案:

异常运行 1 2 Crash

问题6结果


问题7: 下面代码运行结果会什么样?

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"1");

    dispatch_async(dispatch_get_main_queue(), ^{
        
        NSLog(@"2");
        
    });
}

异步分配到串行队列, 这里是主队列。viewDidLoad执行结束之后, 执行异步任务dispatch_async

答案:

运行正常 1 2

问题7结果


问题8: 下面代码运行结果会什么样?

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
   
    dispatch_async(queue, ^{
        
        NSLog(@"1");
        
        [self performSelector:@selector(testAction1)];
        [self performSelector:@selector(testAction2) withObject:nil afterDelay:0];
         
        NSLog(@"4");
        
    });
    
    
}

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


- (void)testAction2 {
    
    NSLog(@"3");
    
}

  • 异步到一个全局队列, 先执行NSLog(@"1");

  • 执行performSelector :方法, 由于performSelector是NSObject上的对象方法, 正常执行

NSObject方法
  • performSelector: withObject: afterDelay:runloop方法, 要提交到runloop上执行, 而GCD底层不会创建runloop的, 所以失效不会执行, 往下走
NSRunLoop方法
  • 1打印完打印2, 不会执行performSelector: withObject: afterDelay: , 接下来打印4, 所以结果1, 2, 4

答案:

运行正常 1 2 4

问题8结果


问题9: 怎样用GCD实现多读单写

例子

例如内存中维护一个字典, 有多个读者和写者操作这块数据

  • 读者读者并发, 可以同一时间可以多个读者读取数据, 相互不受影响
  • 读者写者互斥, 有读者在读取数据, 就不能有写的线程在处理
  • 写者写者互斥, 有写者在写数据, 其他就不能写数据, 防止数据错乱

所以我们设计的模式就是


设计模式

当做写处理的时候通过一个"栅栏"来挡住读处理, 等写处理完成之后再进行读处理

GCD 栅栏函数

dispatch_barrier_async(异步队列, ^{ // 写操作 } );
// barrier: 障碍, 屏障, 隔阂, 关卡, 阻力, 分界线

看个例子, 有三组并发队列, 队列A读取, 队列B更改数据, 队列C读取更改完的数据

如果我们不做任何操作, 如下

@interface ViewController ()

@property(nonatomic, assign) int a;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
 
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    
    dispatch_async(queue, ^{
        NSLog(@"队列A读取: %d", self.a);
    });
    
    __weak typeof(self) weakSelf = self;
    
    dispatch_async(queue, ^{
        
        for (int i = 0; i < 10; i++) {
            weakSelf.a = weakSelf.a + 1;
            
            NSLog(@"队列B数据变更: %d", self.a);
        }
    });
    
    dispatch_async(queue, ^{
        NSLog(@"队列C读取: %d", self.a);
    });
}

错误打印结果


错误示范

很明显并未达到预期, 那么我们加一个栅栏, 改成

    dispatch_async(queue, ^{
        NSLog(@"队列A读取: %d", self.a);
    });
    
    __weak typeof(self) weakSelf = self;
    
//    dispatch_async(queue, ^{
//
//        for (int i = 0; i < 10; i++) {
//            weakSelf.a = weakSelf.a + 1;
//
//            NSLog(@"队列B数据变更: %d", self.a);
//        }
//    });
    
    dispatch_barrier_async(queue, ^{
        
        for (int i = 0; i < 10; i++) {
            weakSelf.a = weakSelf.a + 1;
            
            NSLog(@"栅栏: %d", self.a);
        }
    });
    
    dispatch_async(queue, ^{
        NSLog(@"队列C读取: %d", self.a);
    });
正确结果

可看出这样满足我们的条件



问题10: viewDidLoad中有3组异步并发队列, 每个队列顺序打印10个数, 实现1~30打印

其实这道题也是考察栅栏方法dispatch_barrier_async, 建议先看问题9

如果我们正常写3个并发队列

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        //打印1到10
        for (int i = 1; i < 11; i++) {
            NSLog(@"0到10: %d",i);
        }
    });
    
    dispatch_async(queue, ^{
        //打印11到20
        for (int i = 11; i < 21; i++) {
            NSLog(@"11到20: %d",i);
        }
    });
  
    dispatch_async(queue, ^{
        //打印21到30
        for (int i = 21; i < 31; i++) {
            NSLog(@"21到30: %d",i);
        }
    });

打印结果

错误示范

那么这个时候dispatch_barrier_async就可以起到这个作用

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        //打印1到10
        for (int i = 1; i < 11; i++) {
            NSLog(@"0到10: %d",i);
        }
    });
    
//    dispatch_async(queue, ^{
//        //打印11到20
//        for (int i = 11; i < 21; i++) {
//            NSLog(@"11到20: %d",i);
//        }
//    });
  
    dispatch_barrier_async(queue, ^{
        //打印11到20
        for (int i = 11; i < 21; i++) {
            NSLog(@"11到20: %d",i);
        }
    });
    
    dispatch_async(queue, ^{
        //打印21到30
        for (int i = 21; i < 31; i++) {
            NSLog(@"21到30: %d",i);
        }
    });
    
正确示范


问题11: 使用GCD实现: A, B, C 三个任务并发, 完成后执行任务D

其实这道题就是考察 dispatch_group_async的使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    NSMutableArray *arr = [NSMutableArray array];
    
    dispatch_group_t group = dispatch_group_create();
    
    for (int i = 0; i < 3; i++) {
        
        dispatch_group_async(group, queue, ^{
           
            NSLog(@"执行: %d 当前数组: %@", i, arr);
            [arr addObject:@(i)];
            
        });

    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部执行完: %@", arr);
    });
   
}
运行结果


问题11: 什么是队列

队列: 是一种操作受限制的线性表, 是一种先进先出( FIFO )的数据结构


更加详细参考:

百度百科 队列详解


问题12: 队列和线程关系

  • 队列负责管理任务, 按照FIFO(先进先出)规则将任务交给线程去执行
  • 线程是由线程池和系统进行管理, 跟队列无关, 也无任务概念

其实 线程队列 是两个层级概念. 线程是程序执行的单元, 或者说是系统级的运算调度单位. 队列是线性表, 一种数据结构.


问题13:下面代码运行结果会什么样?

题目

答案:

运行正常 1 5 2 4 3

任务执行, 跟 线程状态, CPU调度, 线程池调度, 队列优先级, 任务复杂度等有关

回到上面题目, 先留意下queue 是 DISPATCH_QUEUE_CONCURRENT 并发队列, 由上到下开始执行, 先打印1.
异步dispatch_async不会堵塞主线程但是会耗时, 系统会先执行后面同步 打印5.
接着执行异步 打印2.
同理里面又含一个异步async, 系统跳过先执行后面 打印4.
接着执行里面异步async 打印3, 结果 1 5 2 4 3

打印结果:

运行结果

如果把里面的dispatch_async 换成 dispatch_sync

变换

结果就会变成 1 5 2 4 3


问题14:下面代码运行结果会什么样?

    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"2");
        
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    
    dispatch_sync(queue, ^{
        NSLog(@"5");
    });
    

答案:

运行正常 1 2 3 4 5

留意下queue 是 DISPATCH_QUEUE_SERIAL 串行队列

打印结果:


运行结果


问题14: vb 面试题, 下面代码运行结果选什么?

    dispatch_queue_t queue = dispatch_queue_create("com.vb.inter", 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");
    });

A: 1230789
B: 1237890
C: 3120798
D: 2137890

1, 2 是dispatch_async异步, 打印位置不一定, 7, 8, 9 同理, 但是3 是dispatch_sync同步以及 0 是直接打印, 会在7, 8, 9 异步前面, 排除B, D. AC都有可能发生, 固 答案: AC

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

推荐阅读更多精彩内容