[iOS] GCD是神马-队列与并发

iOS多线程的处理方式主要有四种:pthread / NSThread / GCD / NSOperation,其中用起来最方便也是最常用的大概就是GCD啦,超级强大的简直崇拜脸~
P.S. 小白这周debug要哭所以今天只写基本概念,下周讨论各种dispatch方法哈~

GCD: Grand Central Dispatch
这个名字英文超赞中文大概就是大型调度中心吧,和它的含义也很类似,你给它一个任务,它来进行调度执行。

我们最常遇到的dispatch大概就是往主线程抛任务了,比如在非主线程的时候往主线程post Notification,以免在接收notification的代码对UI进行了操作导致crash;或将与控件尺寸相关的任务抛到主线程在UI绘制后执行等。


往主线程抛任务.png

创建一个任务并且抛给GCD的主要元素分别是同步异步和队列

(1) 同步异步

同步(dispatch_sync)是在任务执行完才会继续执行后面的内容
异步(dispatch_async)是不用等抛出去的任务执行完就执行后面的代码

NSLog(@"同步任务开始");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
  NSLog(@"同步任务进行中"); 
});
NSLog(@"同步任务结束"); //等待上面的任务做完才会执行
    
    
NSLog(@"异步任务开始");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
  NSLog(@"异步任务进行中");
});
NSLog(@"异步任务结束");  //不等任务做完就执行

输出:
同步任务开始
同步任务进行中
同步任务结束

异步任务开始
异步任务结束
异步任务进行中

(2) 队列

队列主要分两大类——串行队列和并行队列
串行队列:顺序执行放进来的任务,执行完一个再执行下一个(不一定是只有一个线程,但一个队列内一定是顺序执行)
并行队列:可以同时执行多个任务(可能会开启多个线程)

串行队列

并行队列

※那么要如何得到队列呢?

(i) 获取主队列,串行 -> dispatch_get_main_queue()
(ii) 获取全局并发队列 -> dispatch_get_global_queue()
我们经常使用dispatch_get_global_queue(0, 0)来获取全局queue,那么这俩参数是啥呢?第一个就是优先级,而第二个flag其实现在木有用,但是官方说传0(传1返回nil,即不会执行任务但不crash;传2是隐藏操作,可以在当前没有可用线程时不等待单开线程)。

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
global queue参数.png

在任务加入时间相近的时候,优先级高的任务队列会优先执行

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    NSLog(@"4");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    NSLog(@"3");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"2");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"1");
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

输出:
1
2
current thread: <NSThread: 0x60000362ca80>{number = 4, name = (null)}
current thread: <NSThread: 0x6000036dc440>{number = 5, name = (null)}
3
current thread: <NSThread: 0x60000362ca80>{number = 4, name = (null)}
4
current thread: <NSThread: 0x6000036dc440>{number = 5, name = (null)}

可以看到thread还是在复用的,只是优先级高的先执行罢了。

用不同优先级创建出的队列是不一样的哦,高优先级的队列会分到更多的时间片,被执行的概率会更高,也就更容易早一点被执行。

global queue优先级.png

(iii) 创建串行/并行队列 -> dispatch_queue_create()

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

自己创建queue只要给出一个label&是串行/并行就可以啦(DISPATCH_QUEUE_SERIAL / DISPATCH_QUEUE_CONCURRENT)。

注意给出的label并不是queue的唯一标识符,不给都可以,只是一个和queue绑定的字符串,即使我们给出相同的label,返回的仍旧是不同的queue,这个label主要是让我们给自己一个分辨是哪个queue的方式,如果想作为标识符,需要人为确保名称唯一。

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
    
NSLog(@"serialQueue地址为:%p", serialQueue);
NSLog(@"serialQueue2地址为:%p", serialQueue2);

输出:
serialQueue地址为:0x600000ed3100
serialQueue2地址为:0x600000ed3180

ARC已经可以自己管理队列的生命周期了,如果block都执行完了(block其实持有了queue的引用),并且没有其他引用指向queue了,它就会自己销毁掉,不需要我们手动调用dispathc_release了哦。


(3) 排列组合

这里来尝试一下各种可能性~

① 异步+主队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(dispatch_get_main_queue(), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 0
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 1
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 2
current thread: <NSThread: 0x600000828b80>{number = 1, name = main}
current task: 3

话说这个NSThread的number什么呢?
根据https://stackoverflow.com/questions/15558411/nsthread-number-on-ios的解释,其实他就是一个线程的序号没多大意义,正常都不会用到的

队列的规则都是FIFO(first in first out,先入先出),所以如果往主队列连续抛四个异步任务,因为主队列只有一个主线程,所以会按照抛入的顺序执行,并且在一个执行完毕后再执行下一个。

② 异步+全局并发队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x60000270f680>{number = 3, name = (null)}
current thread: <NSThread: 0x600002798480>{number = 5, name = (null)}
current thread: <NSThread: 0x60000278ae00>{number = 7, name = (null)}
current thread: <NSThread: 0x60000278e340>{number = 6, name = (null)}
current task: 1
current task: 2
current task: 3
current task: 0

全局并发队列可能会用多个线程执行任务,可以看到四个任务使用了4个不同的线程,对于每个线程而言,抛给它的任务其实是串行的,但是因为有多个线程,所以给global queue的任务是并行的。

队列不是FIFO么,为什么task 0不是第一个执行的?
如果有线程空闲了,会自己找队列要任务,那么task 0会被第一个分配给线程0x60000278e340,但是线程什么时候可以执行任务要看CPU分配的时间片,虽然他空闲但是不一定现在有拿到时间片,毕竟线程的并发其实都是交替执行。所以对于并发队列,第一个被抛进去不代表会最先被执行,具体什么时候执行要看thread的状况。

③ 异步+创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(serialQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 0
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 1
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 2
current thread: <NSThread: 0x600000ed0040>{number = 5, name = (null)}
current task: 3

和①异步+主队列类似,创建一个串行队列也是会顺序执行抛入的任务,区别只是它并不会在主线程执行这些任务。

④ 异步+创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_async(concurrentQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x6000023472c0>{number = 3, name = (null)}
current task: 1
current thread: <NSThread: 0x60000239d0c0>{number = 5, name = (null)}
current task: 3
current thread: <NSThread: 0x6000023472c0>{number = 3, name = (null)}
current task: 2
current thread: <NSThread: 0x60000234da80>{number = 4, name = (null)}
current task: 0

和②异步+全局队列类似,创建并行队列会使用多个线程,并且task执行顺序并不是抛入的顺序。使用现有的thread还是开启几个新thread会由GCD来决定的哦。

⑤ 同步+主队列

如果在主线程运行同步+主队列会造成死锁crash哈,因为主线程在等block运行完,而block在等主线程有空闲,于是两个人互相等待就造成了死锁。 (这里的解释其实是有问题的,下一个例子会详细讲死锁怎么产生)

//会crash哦
//主线程等待block结束后继续运行
dispatch_sync(dispatch_get_main_queue(), ^{
    //抛给主线程的任务,等待主线程有空闲的时候运行
    NSLog(@"current thread: %@", [NSThread currentThread]);
});

如果想解除crash,可以dispatch_async给一个global queue,在异步任务里面dispatch_sync给主队列。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"current thread: %@", [NSThread currentThread]);
    });
});

但其实自己给自己抛同步任务完全木有必要哦-。-

⑥ 同步+全局并发队列
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x600001316940>{number = 1, name = main}
is main thread: yes
current task: 3

global queue是并发队列,但是如果是同步任务,任务会依次在global queue里的主线程执行,不会用多个线程,因为分配给了一个线程所以就是顺序的啦,不会像异步那种顺序错乱。

dispatch_sync给global queue实际在主线程执行,那么为什么不会死锁呢?
其实造成死锁的不是主线程在等待主线程执行结束,而是主队列在等待主队列,如果不是同一个队列是OK的

※但不是所有global queue + sync任务都在主线程执行哦~

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
  NSLog(@"serialQueue thread:%@", [NSThread currentThread]);
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"serialQueue task thread:%@", [NSThread currentThread]);
  });
});

输出:
serialQueue thread:<NSThread: 0x600001849b80>{number = 5, name = (null)}
serialQueue task thread:<NSThread: 0x600001849b80>{number = 5, name = (null)}

如果我们新建一个串行队列,里面的任务并不在主线程执行,然后抛一个同步任务,发现它运行的线程和当前线程是一致的,所以其实同步任务就是执行在当前线程


※什么时候会死锁?

main函数默认是运行在主队列的,也就是我们dispatch_get_main_queue得到的queue,当我们在主队列里面给主队列抛一个同步任务,会造成后面抛入的任务在前面的任务执行完毕,前面的又在等后面的。


主队列死锁

基于先入先出原则,只有A执行完毕才能执行B,但A任务内要等待后被抛入的任务B执行完才能继续执行,所以有死锁问题。

在串行队列中同步等待串行队列容易发生死锁

例如执行下面的代码,虽然不会crash,但是控制台只输出了“任务1开始”。

dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
    NSLog(@"任务1开始");
        
    dispatch_sync(dispatch_get_main_queue(), ^{
      NSLog(@"任务2");
    });

    NSLog(@"任务1结束");
});
    
dispatch_sync(serialQueue, ^{
    NSLog(@"任务3");
});
死锁.png

幸好这种死锁不会crash,一个圈的互相等待会比较不容易发现,所以日常用GCD的时候如果是串行同步的话需要格外注意一下哈。

为什么并行队列执行同步任务等待自己队列不会死锁呢?
因为主队列和串行队列,当队列中有任务在执行时就会暂停调度,等待调度中任务执行完毕后再执行后面的任务,但是并行队列没有这个限制。

死锁示例:
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    dispatch_sync(serialQueue, ^{
        NSLog(@"sync task");
    });
    
    NSLog(@"finished");
});

不死锁示例:
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"sync task");
    });
    
    NSLog(@"finished");
});
⑦ 同步+创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(serialQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x600001b32880>{number = 1, name = main}
is main thread: yes
current task: 3

和全局并行一样,同步+创建串行队列使用的也仍旧是当前线程(这里是主线程)。

其实同一个队列不一定使用同一个线程,当我们用异步+串行队列的时候,会新建一个线程执行,但如果是同步的任务,会在当前线程执行。

⑧ 同步+创建并行队列

同上哈,也是会顺序执行并且用当前线程。那么它和用同步串行的区别是啥呢?就同步任务其实没啥区别,但如果你还往里面抛异步任务就有区别啦。

dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 4; i++) {
  dispatch_sync(concurrentQueue, ^{
      NSLog(@"current thread: %@", [NSThread currentThread]);
      NSLog(@"is main thread: %@", [NSThread currentThread] == [NSThread mainThread] ? @"yes" : @"no");
      [NSThread sleepForTimeInterval:1];
      NSLog(@"current task: %ld", (long)i);
  });
}

输出:
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 0
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 1
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 2
current thread: <NSThread: 0x6000016cd3c0>{number = 1, name = main}
is main thread: yes
current task: 3

(4) 总结

/ 主队列 全局队列 串行队列 并行队列
同步 主线程运行(可能死锁) + 串行 当前线程运行 + 串行 当前线程运行 + 串行 当前线程运行 + 串行
异步 主线程运行 + 串行 多线程 + 并行 新线程 + 串行 多线程 + 并行

参考文章:
1.『GCD』详尽总结:https://www.jianshu.com/p/2d57c72016c6

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

推荐阅读更多精彩内容