GCD中队列与任务嵌套的组合情况分析

前言

标题可能有点令人费解,解释一下。众所周知,GCD编程是面向队列和任务的,无需关心线程的创建和维护。GCD中有两种队列和两种任务,不同队列和不同任务组合起来往往就容易被绕晕。本文就队列和任务的四种组合情况,结合任务间嵌套进行分析。

系好安全带

  • 队列
    GCD中有两种队列:串行队列和并行队列。队列里面存放要执行的任务,既然是队列,那么一定遵循先进先出的原则。比如往队列中先添加A任务再添加B任务,则一定是A任务先执行,B任务再执行。不管串行队列还是并行队列,都是要遵守这个原则的。
    而串行队列和并行队列的区别在于,串行队列中,一定是先取A任务并执行,等到A任务执行完毕后才取B任务并执行。并行队列同样是先取A任务再取B任务,但不必等到A执行完再取B,而是在A任务还在执行的时候,就可以取出B任务并执行。
    注意:千万不要认为,并行队列可以不顾任务添加顺序而"并行"地取多个任务,队列就应该有个队列的样子o(╯□╰)o

  • 任务
    GCD有两种任务:同步任务和异步任务。这俩的区别在于,是否阻塞当前线程。什么意思呢?向某个队列添加任务,这个操作,肯定是在某个线程里执行的,比如主线程。如果是同步任务,则一定是先把这个任务执行完了,才可以继续往下执行,比如下面代码:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        /*
            向队列中添加任务的代码
        */
        
        NSLog("GCD test");
    }
    

如果是同步任务,那只能是等这个任务执行完之后,才会来到NSLog那里,即使这个任务有好多循环,进行了大量复杂的计算,也只能是这样,谁叫这是个同步任务呢。若是异步任务,那执行完添加任务的那部分代码后,直接往后走,那个异步任务在合适的时候再执行,一般那个异步任务会在一个新的线程里执行。
小结一下,同步任务会阻塞当前线程,一定是在当前线程下执行,GCD不会开启新的线程;而异步任务,不会阻塞当前线程,当前线程会继续执行后面的代码,一般这个异步任务会在一个新的线程中执行,但这不是绝对的,并不是说添加了一个异步任务后GCD就会开启一个新的线程,后面有例子。

发车啦

下面将对队列和任务组合,结合任务的嵌套进行分析

串行队列的同步任务

这种情况最简单,执行的结果是可预测的,比如下面代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"inside -- %@",[NSThread currentThread]);
    });
    NSLog(@"outside -- %@",[NSThread currentThread]);
}

运行结果如下:

inside -- <NSThread: 0x7fcfe9704e70>{number = 1, name = main}
outside -- <NSThread: 0x7fcfe9704e70>{number = 1, name = main}

由于是同步任务,会先执行这个同步任务再往下执行,实际上跟下面这个效果是一样的:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"inside -- %@",[NSThread currentThread]);
    NSLog(@"outside -- %@",[NSThread currentThread]);
}

呵呵。
等等,说好的将队列和任务的组合,怎么只讲任务了。如果要组合起来的话,就使用任务的嵌套吧。修改一下上面的代码

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_sync(serialQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
        });
    });
}

注意:现在就不在主线程里打印日志了,注意力全部放在队列和任务的组合上
运行结果如下:

outside -- <NSThread: 0x7fe7c2700360>{number = 1, name = main}

如果outside的那个NSLog写在当前任务的最后,也就是放在第二个同步任务的后面,运行结果是什么都不会打印。
好,现在使用刚才讲的队列和任务的特点分析一下。
首先外层的任务是一个同步任务,会阻塞当前线程,这里的当前线程就是主线程了,因为是在viewDidLoad里面嘛。那就执行这个同步任务呗,由于该任务是添加在serialQueue队列中,所以从这个队列里面取出这个同步任务并执行,执行到outside的那个log时打印出了当前线程,结果是主线程,因为阻塞了主线程嘛,自然就在主线程中运行了。
接着往下执行,又往serialQueue中添加一个任务,这又是个同步任务,阻塞当前线程,要执行完了这个任务才能接着往下执行。那就把这个任务取出来,赶快执行掉好往下走啊。很遗憾,serialQueue是一个串行队列,现在正在执行第一个同步任务,就是有outside打印的那个同步任务,所以serialQueue取不出inside那个任务。所以,inside这个任务要等outside执行才能执行。另一方面,由于第二个任务(inside)是同步任务,一定要执行完这个同步任务才能往下走,所以outside要等inside这个任务执行完了,才能往下走,才能走完,这个任务才得以结束。所以outside要等inside执行完才能执行。这两个任务就这样互相掐着,永远不能执行或者执行完毕。更悲惨的是,这两个任务阻塞的是主线程,那主线程就没办法再继续走下去了。解决的方法,可以将两个任务添加到不同的队列中,两个队列取任务互不干扰,就不会出现“互掐”的情况了。或者将队列改成并行队列,或者将第二个任务改成异步任务,这些都会在下面的组合情况中看到。
这也就解释了,为什么在主队列上只能添加异步任务。首先主队列是一个串行队列,其次程序启动后,主队列中一直有一个任务,不断地监听用户输入及各种事件。所以如果在主线程中,往主队列中添加同步任务,则这个同步任务会阻塞主线程,而且永远取不出来,应用中断不能进行下去。如果是在其他线程向主队列添加同步任务,则主线程不会影响,但这个同步任务是永远不会执行到了。
小结:同一个串行队列不能嵌套同步任务,注意是同一个哦。

串行队列的异步任务

接刚才的例子,如何使用异步任务解决刚才的问题。刚才的问题是两个任务都依赖着彼此,所以都不能执行或者执行完毕。所以只要解除其中任意一条就行。就本节而言,因为要使用异步任务,所以解决方式就是不让outside依赖inside的执行,也就是说inside这个任务改成异步任务,代码只需要加一个字母😳

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_async(serialQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
        });
    });
}

inside的那个任务由sync改成async,打印结果如下:

outside -- <NSThread: 0x7ff57940a560>{number = 1, name = main}
inside -- <NSThread: 0x7ff5796102a0>{number = 2, name = (null)}

可以看到,inside任务在一个新的线程,number = 2中执行。
值得注意的是,尽管inside任务是在一个新的线程中执行的,但inside还是要等到outside执行完才能执行,所以考虑以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_async(serialQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}

运行结果是可预测的,一定是outside先打印完100个数据后,inside再开始打印,因为是串行队列,所以一定要outside执行完后才执行inside任务。

outside -- <NSThread: 0x7f975a509d40>{number = 1, name = main}
outside -- 1
outside -- 2
...
outside -- 100
inside -- <NSThread: 0x7f975a409790>{number = 2, name = (null)}
outside -- 1
outside -- 2
...
outside -- 100

那如果外面是异步任务,里面是同步任务呢?外面的异步任务只是不会阻塞主线程,会开启一个新的线程,但内层的同步任务就会阻塞这个新建的线程,但由于又是串行队列,永远取不到这个同步任务。所以外层的异步任务永远不会执行完,内层的同步任务永远不会被取出并执行。相对第一种情况(两个同步任务)而言,只是没有阻塞主线程而已。
如果两个任务都是异步任务,则两个任务都会顺利地执行。这里需要注意的是,外层的异步任务,GCD会开启一个新的线程,而内层的任务,虽然是异步任务,但GCD不会开启新的线程,而是在与外层任务相同的线程中执行。因为这是个串行队列,必须外层先执行完才能轮到内层执行,所以没有必要再开一个新的线程执行,并行队列就不一样咯。这就是上面所说的,异步任务不一定会使GCD开启一个新的线程。代码和运行结果如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t serialQueue = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_async(serialQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}
outside -- <NSThread: 0x7fd723db43c0>{number = 2, name = (null)}
outside -- 1
outside -- 2
...
outside -- 100
inside -- <NSThread: 0x7fd723db43c0>{number = 2, name = (null)}
inside -- 1
inside -- 2
...
inside -- 100

可见outsideinside都是在number = 2的线程中执行的。

并行队列的同步任务

刚才说了,只要解除两个任务中的任意一条依赖就行。使用串行队列的异步任务是解除outsideinside的依赖,使得outside可以顺利结束。现在试着解除insideoutside的依赖,要解除这种依赖,就要在outside还在执行的时候,就可以取出inside,并行队列正好可以解决这个问题。代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
        });
    });
}

没什么大改动,只是把队列改成了并行队列。如果再加上刚才的循环呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}

因为inside任务是同步任务,阻塞了outside的执行,所以要先执行完insideoutside才能接着执行,所以这种情况,运行结果是可预测的。结果如下:

outside -- <NSThread: 0x7feef8707490>{number = 1, name = main}
inside -- <NSThread: 0x7feef8707490>{number = 1, name = main}
inside -- 1
inside -- 2
...
inside -- 100
outside -- 1
outside -- 2
...
outside -- 100

并行队列的异步任务

考虑以下三种情况。
外层异步任务,内层同步任务,代码

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}

这种情况,要是放在串行队列中,这两个任务就不能顺利执行了。但因为是并行队列,当外层的异步任务还在执行的时候,是可以取出内层的同步任务的。注意,虽然内层的同步任务阻塞了当前线程,但并行队列可以将其取出并在当前线程中执行,执行完后,外层的异步任务再继续往下执行。所以运行结果是可预测的。

outside -- <NSThread: 0x7ff388f10630>{number = 2, name = (null)}
inside -- <NSThread: 0x7ff388f10630>{number = 2, name = (null)}
inside -- 1
inside -- 2
...
inside -- 100
outside -- 1
outside -- 2
...
outside -- 100

外层同步任务,内层异步任务,代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_async(concurrentQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}

外层的同步任务,在主线程中运行。当执行到内层的任务时,由于是异步任务,所以不会阻塞当前线程,添加完任务后继续往后面走,而且由于是并行队列,所以这个异步任务在外层同步任务执行完之前,可以被取出并执行,当然,是在一个新的线程里面执行,也就是说这两个任务在不同的线程中并行地在执行,所以结果是不可预测的。我这里的运行结果是这样的:

outside -- <NSThread: 0x7ff45bf07400>{number = 1, name = main}
outside -- 1
inside -- <NSThread: 0x7ff45bdb1a40>{number = 2, name = (null)}
outside -- 2
inside -- 1
outside -- 3
inside -- 2
...

外层和内层任务都是异步任务。这种情况相对上面来说是最复杂的,嗯。代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"outside -- %@",[NSThread currentThread]);
        dispatch_async(concurrentQueue, ^{
            NSLog(@"inside -- %@",[NSThread currentThread]);
            for (NSInteger idx = 1; idx <= 100; idx++) {
                NSLog(@"inside -- %@",@(idx));
            }
        });
        for (NSInteger idx = 1; idx <= 100; idx++) {
            NSLog(@"outside -- %@",@(idx));
        }
    });
}

其实,相对于外层同步内层异步的情况,只是外层任务变成了异步,那就不会阻塞主线程了,而是在一个新的线程中运行,当走到内层的异步任务时,不会阻塞这个新建的线程,又由于这是并行队列,所以可以把内层的异步任务取出,所以,两个任务又是在不同的线程中并行的执行,只不过外层的异步任务不是在主线程中执行。运行结果同样是不可预测的:

outside -- <NSThread: 0x7f9d2a535470>{number = 2, name = (null)}
outside -- 1
outside -- 2
inside -- <NSThread: 0x7f9d2a5386c0>{number = 3, name = (null)}
outside -- 3
inside -- 1
outside -- 4
inside -- 2
...

可以看到,两个任务分别在不同的线程(number = 2, number =3)中执行。

总结

  1. 同一串行队列下,任务的嵌套,内部只能嵌套异步任务,这也就是主队列中只能添加异步任务的原因;
  2. 同一串行队列中的任务运行结果总是可预测但可能造成“互掐”的情况,导致某些任务不能正确执行;
  3. 并行队列中的任务,不管如何组合都不会造成“互掐”的情况,但某些运行结果不可预测;
  4. 牢记队列和任务各自的职责和特点,就不容易绕晕了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容