iOS多线程实现——GCD使用详解

一、介绍

GCD,英文全称是Grand Central Dispatch(功能强悍的中央调度器),基于C语言编写的一套多线程开发机制,因此使用时会以函数形式出现,且大部分函数以dispatch开头,虽然是C语言的但相对于苹果其它多线程实现方式,抽象层次更高,使用起来也更加方便。

它是苹果为应对多核的并行运算提出的解决方案,它会自动利用多核进行并发处理和运算,它能提供系统级别的处理,而不再局限于某个进程、线程,官方声称会更快、更高效、更灵敏,且线程由系统自动管理(调度、运行),无需程序员参与,使用起来非常方便。

二、任务和队列

GCD有两个核心:任务和队列。

任务:要执行的操作或方法函数,队列:存放任务的集合,而我们要做的就是将任务添加到队列然后执行,GCD会自动将队列中的任务按先进先出的方式取出并交给对应线程执行。注意任务的取出是按照先进先出的方式,这也是队列的特性,但是取出后的执行顺序则不一定,下面会详细讨论。

1 任务

任务是一个比较抽象的概念,可以简单的认为是一个操作、一个函数、一个方法等等,在实际的开发中大多是以block(block使用详见)的形式,使用起来也更加灵活。

2 队列queue

  • 有两种队列:串行队列和并行队列。串行队列:同步执行,在当前线程执行;并行队列:可由多个线程异步执行,但任务的取出还是FIFO的

队列创建,根据函数第二个参数来创建串行或并行队列。

// 参数1 队列名称
// 参数2 队列类型 DISPATCH_QUEUE_SERIAL/NULL串行队列,DISPATCH_QUEUE_CONCURRENT代表并行队列
// 下面代码为创建一个串行队列,也是实际开发中用的最多的
dispatch_queue_t serialQ = dispatch_queue_create("队列名", NULL);
  • 另外系统提供了两种队列:全局队列和主队列。

全局队列属于并行队列,只不过已由系统创建的没有名字,且在全局可见(可用)。获取全局队列:

/* 取得全局队列
 第一个参数:线程优先级,设为默认即可,个人习惯写0,等同于默认
 第二个参数:标记参数,目前没有用,一般传入0
 */
serialQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

主队列属于串行队列,也由系统创建,只不过运行在主线程(UI线程)。获取主队列:

// 获取主队列
serialQ = dispatch_get_main_queue();
  • 关于内存:queue属于一个对象,也是占用内存的,也会使用引用计数,当向queue添加一个任务时就会将这个queue retain一下,引用计数+1,直到所有任务都完成内存才会释放。(我们在声明一个queue属性时要用strong)。

3 执行方式——2种

同步执行和异步执行。

  • 同步执行:不会开启新的线程,在当前线程执行。
  • 异步执行:gcd管理的线程池中有空闲线程就会从队列中取出任务执行,会开启线程。

下面为实现同步和异步的函数,函数功能为:将任务添加到队列并执行。

/* 同步执行
 第一个参数:执行任务的队列:串行、并行、全局、主队列
 第二个参数:block任务
 */
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 异步执行
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

注意:默认情况下,新线程都没有开启runloop,所以当block任务完成后,线程都会自动被回收,假设我们想在新开的线程中使用NSTimer,就必须开启runloop,可以使用[[NSRunLoop currentRunLoop] run]开启当前线程,这是就要自己管理线程的回收等工作。

  • 另外还有两个方法,实际开发中用的并不是太多

dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

加了一个barrier,意义在于:队列之前的block处理完成之后才开始处理队列中barrier的block,且barrier的block必须处理完之后,才能处理其它的block。

根据这个特性我们可以实现123456一共6个block,可以让特定几个并发执行完成之后,再并发执行剩下的block。比如123先并发,之后456再并发执行。具体代码如下(将barrier放在123与456之间即可):

- (void)barrierTest {
    // 1 创建并发队列
    dispatch_queue_t BCqueue = dispatch_queue_create("BarrierConcurrent", DISPATCH_QUEUE_CONCURRENT);
    
    // 2.1 添加任务123
    dispatch_async(BCqueue, ^{
        NSLog(@"task1,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        sleep(3);
        NSLog(@"task2,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        sleep(1);
        NSLog(@"task3,%@", [NSThread currentThread]);
    });
    // 2.2 添加barrier
    dispatch_barrier_async(BCqueue, ^{
        NSLog(@"barrier");
    });
    // 2.3 添加任务456
    dispatch_async(BCqueue, ^{
        sleep(1);
        NSLog(@"task4,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        NSLog(@"task5,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        NSLog(@"task6,%@", [NSThread currentThread]);
    });
}

输出结果,为了显示效果,代码有延时操作:

gcdBarrierTest.png

三、几种类型

很明显两种执行方式,两种队列。那么就有4种情况:串行队列同步执行、串行队列异步执行、并行队列同步执行、并行队列异步执行。哪一种会开启新的线程?开几条?是否并发?记忆起来比较绕,但是只要抓住基本的就可以,为了方便理解,现分析如下:

  1. 串行队列,同步执行-----串行队列意味着顺序执行,同步执行意味着不开启线程(在当前线程执行)
  1. 串行队列,异步执行-----串行队列意味着任务顺序执行,异步执行说明要开线程, (如果开多个线程的话,不能保证串行队列顺序执行,所以只开一个线程)
  2. 并行队列,异步执行-----并行队列意味着执行顺序不确定,异步执行意味着会开启线程,而并行队列又允许不按顺序执行,所以系统为了提高性能会开启多个线程,来队列取任务(队列中任务取出仍然是顺序取出的,只是线程执行无序)。
  3. 并行队列,同步执行-----同步执行意味着不开线程,则肯定是顺序执行
  4. 死锁-----程序执行不出来(死锁) ;

四、死锁举例

  • 主队列死锁:

这种死锁最常见,问题也最严重,会造成主线程卡住。原因:主队列,如果主线程正在执行代码,就不调度任务;同步执行:一直执行第一个任务直到结束。两者互相等待造成死锁,示例如下:

- (void)mainThreadDeadLockTest {
    NSLog(@"begin");
    dispatch_sync(dispatch_get_main_queue(), ^{
        // 发生死锁下面的代码不会执行
        NSLog(@"middle");
    });
    // 发生死锁下面的代码不会执行,当然函数也不会返回,后果也最为严重
    NSLog(@"end");
}
  • 在其它线程死锁,这种不会影响主线程:

原因:serialQueue为串行队列,当代码执行到block1时正常,执行到dispatch_sync时,dispatch_sync等待block2执行完毕才会返回,而serialQueue是串行队列,它正在执行block1,只有等block1执行完毕后才会去执行block2,相互等待造成死锁

- (void)deadLockTest {
    // 其它线程的死锁
    dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        // 串行队列block1
        NSLog(@"begin");
        dispatch_sync(serialQueue, ^{
            // 串行队列block2 发生死锁,下面的代码不会执行
            NSLog(@"middle");
        });
        // 不会打印
        NSLog(@"end");
    });
    // 函数会返回,不影响主线程
    NSLog(@"return");
}

五、常用举例

  • 线程间通讯

比如,为了提高用户体验,我们一般在其他线程(非主线程)下载图片或其它网络资源,下载完成后我们要更新UI,而UI更新必须在主线程执行,所以我们经常会使用:

// 同步执行,会阻塞指导下面block中的代码执行完毕
dispatch_sync(dispatch_get_main_queue(), ^{
    // 主线程,UI更新
});
// 异步执行
dispatch_async(dispatch_get_main_queue(), ^{
    // 主线程,UI更新
});
  • 信号量的使用

也属于线程间通讯,下面的举例是经常用到的场景。在网络访问中,NSURLSession类都是异步的(找了很久没有找到同步的方法),而有时我们希望能够像NSURLConnection一样可以同步访问,即在网络block调用完成之后做一些操作。那我们可以使用dispatch的信号量来解决:

/// 用于线程间通讯,下面是等待一个网络完成
- (void)dispatchSemaphore {
    NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    // 设置缓存策略为每次都从网络加载 超时时间30秒
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 处理完成之后,发送信号量
        NSLog(@"正在处理...");
        dispatch_semaphore_signal(semaphore);
    }] resume];
    // 等待网络处理完成
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"处理完成!");
}

在上面的举例中dispatch_semaphore_signal的调用必须是在另一个线程调用,因为当前线程已经dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主线程调用

  • 全局队列,实现并发:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 要执行的代码
});

六、Dispatch Group调度组

使用调度组,可以轻松实现在一些任务完成后,做一些操作。比如具有顺序性要求的生产者消费者等等。

  • 示例1:任务1完成之后执行任务2。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self groupTest];
}
- (void)groupTest {
    // 创建一个组
    dispatch_group_t group = dispatch_group_create();
    NSLog(@"开始执行");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 任务1
            // 等待1s一段时间在执行
            [NSThread sleepForTimeInterval:1];
            NSLog(@"task1 running in %@",[NSThread currentThread]);
        });
        dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
            // 任务2
            NSLog(@"task2 running in %@",[NSThread currentThread]);
        });
    });
}

点击屏幕后,打印如下,可以看到任务1虽然等待了1s,任务2也不执行,只有任务1执行完毕才执行任务2.

2015-08-28 18:16:05.317 GCDTest[1468:229374] 开始执行
2015-08-28 18:16:06.323 GCDTest[1468:229457] task1 running in <NSThread: 0x7f8962f16900>{number = 2, name = (null)}
2015-08-28 18:16:06.323 GCDTest[1468:229456] task2 running in <NSThread: 0x7f8962c92750>{number = 3, name = (null)}
  • 示例2:其实示例1并不常用,真正用到的是监控多个任务完成之后,回到主线程更新UI,或者做其它事情。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self groupTest];
}
- (void)groupTest {
    // 创建一个组
    dispatch_group_t group = dispatch_group_create();
    NSLog(@"开始执行");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 关联任务1
            NSLog(@"task1 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 关联任务2
            NSLog(@"task2 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 关联任务3
            NSLog(@"task3 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 关联任务4
            // 等待1秒
            [NSThread sleepForTimeInterval:1];
            NSLog(@"task4 running in %@",[NSThread currentThread]);
        });
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            // 回到主线程执行
            NSLog(@"mainTask running in %@",[NSThread currentThread]);
        });
    });
}

点击屏幕后,打印如下,可以看到无论其它任务然后和执行,mainTask等待它们执行后才执行。

2015-08-28 18:24:14.312 GCDTest[1554:236273] 开始执行
2015-08-28 18:24:14.312 GCDTest[1554:236352] task3 running in <NSThread: 0x7fa8f1f0c9c0>{number = 4, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236354] task1 running in <NSThread: 0x7fa8f1d10750>{number = 2, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236351] task2 running in <NSThread: 0x7fa8f1c291a0>{number = 3, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236353] task4 running in <NSThread: 0x7fa8f1d0e7f0>{number = 5, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236273] mainTask running in <NSThread: 0x7fa8f1c13df0>{number = 1, name = main}

关于Dispatch对象内存管理问题

根据上面的代码,可以看出有关dispatch的对象并不是OC对象,那么,用不用像对待Core Foundation框架的对象一样,使用retain/release来管理呢?答案是不用的!

如果是ARC环境,我们无需管理,会像对待OC对象一样自动内存管理。
如果是MRC环境,不是使用retain/release,而是使用dispatch_retain/dispatch_release来管理。

参考:http://www.cnblogs.com/mddblog/p/4767559.html

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

推荐阅读更多精彩内容