iOS多线程之三:GCD的使用

一、什么是 GCD
GCDGrand Central Dispatch 的简称,它是基于 C 语言的。如果使用GCD 完全由系统管理线程,不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的调度队列 dispatch queueGCD 会负责创建线程和调度你的任务,系统直接提供线程管理。

二、GCD 任务和队列
首先看下这段代码:

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

上面的这段代码是一个简单的异步任务,通过这段代码,引出了下面的几个名词:
1、异步执行(async)与同步执行(sync):

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    只能在当前线程中执行任务,不具备开启新线程的能力。

  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    可以在新的线程中执行任务,具备开启新线程的能力;

异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。

2、队列
队列的基本原理:先进先出(FIFO)的原则,先进队列的元素先出队列。
GCD 中,常见的两种队列:串行队列和并发队列;

串行队列(Serial Dispatch Queue):
串行队列中,只开启一个线程,一个任务执行完毕之后,在执行下一个任务;
并发队列(Concurrent Dispatch Queue):
并发队列中,可以开启多个线程,多个任务同时并发执行;

三、GCD 的使用
1、队列的创建方法 / 获取方法
串行队列(Serial Dispatch Queue)和并发队列(Concurrent Dispatch Queue)

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.serial.testQueue", DISPATCH_QUEUE_SERIAL); 
// 并发队列的创建方法 
dispatch_queue_t queue = dispatch_queue_create("com.concurrent.testQueue", DISPATCH_QUEUE_CONCURRENT);
  • 第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
  • 第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。

主队列(Main Dispatch Queue)

// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

全局并发队列(Global Dispatch Queue)

// 全局并发队列的获取方法 
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

通过 dispatch_get_global_queue 方法获取的全局队列都是并行队列,并且队列不能被修改。
identifier:用以标识队列优先级;
flags:苹果预留的,第二个参数暂时没用,用 0 即可;

2、任务和队列不同组合方式的区别

区别 并发队列 串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 会发生死锁,造成 crash
异步(async) 有开启新线程,并发执行任务 有开启新线程(1条),并发执行任务 没有开启新线程,串行执行任务

3、主队列同步造成死锁的原因
dispatch_sync 函数本身是放在主线程中执行的,也就是说他本身也是属于主线程执行任务的一部分。根据主线程的特点:主线程会等主线程上的代码执行完毕之后才会去执行放置到主队列中的 task;再根据 disptach_sync 函数特点, task 不执行完毕,dispatch_sync 函数不返回。这样,dispatch_sync 为了返回会等 task 执行完毕也就是主线程执行完,而 task 执行又等着主线程上的代码执行完,也即主线程上 dispatch_sync 代码执行完。两个任务互相等待,造成死锁;

/** 
主队列同步 
*/
- (void)syncMain {

    NSLog(@"\n\n**************主队列同步,放到主线程会死锁***************\n\n");

    // 主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"主队列同步1   %@",[NSThread currentThread]);
        }
    });
}

4、同步执行 + 并发队列
只会在当前线程中依次执行任务,不会开启新线程,执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。

- (void)testConcurrentQueueAsynExecution {
    // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_CONCURRENT);
    // 第一个任务
    dispatch_sync(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第二个任务
    dispatch_sync(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第三个任务
    dispatch_sync(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
    });
    NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}

虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。

5、异步执行 + 并发队列
可以开启多个线程,任务交替(同时)执行;

- (void)testConcurrentQueueSyncExecution {
    // dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    // 全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 第一个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第二个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第三个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
    });
    NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}

从 log 中可以发现,系统另外开启了3条线程,并且任务是同时执行的,并不是按照1>2>3顺序执行。所以异步+并发队列具备开启新线程的能力,且并发队列可开启多个线程,同时执行多个任务。

6、同步执行 + 串行队列
只会在当前线程中依次执行任务,不会开启新线程,执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则,并且不会产生新的线程。

- (void)testSerialQueueAsynExecution {
    dispatch_queue_t queue = dispatch_queue_create("com.test.syncQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"任务一");
        NSLog(@"currentThread:%@", [NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        NSLog(@"任务二");
        NSLog(@"currentThread:%@", [NSThread currentThread]);
    });
    
    NSLog(@"任务三");
}

7、异步执行 + 串行队列
开启了一条新线程,异步执行具备开启新线程的能力且只开启一个线程,在该线程中执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。

- (void)testSerialQueueSyncExecution {
    dispatch_queue_t queue = dispatch_queue_create("com.test.syncQueue", DISPATCH_QUEUE_SERIAL);
    // 第一个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第二个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第三个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
    });
}

8、同步执行 + 主队列
直接crash,这是因为发生了死锁,在 GCD 中,禁止在主队列(串行队列)中再以同步操作执行主队列任务。同理,在同一个同步串行队列中,再使用该队列同步执行任务也是会发生死锁。

- (void)testMainQueueAsynExecution {
    dispatch_queue_t queue = dispatch_queue_create("com.test.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----11111-----当前线程%@", [NSThread currentThread]);//到这里就死锁了
        dispatch_sync(queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"----22222---当前线程%@", [NSThread currentThread]);
        });
        NSLog(@"----333333-----当前线程%@", [NSThread currentThread]);
    });
    NSLog(@"----44444-----当前线程%@", [NSThread currentThread]);
}

9、异步执行 + 主队列
所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中),在主线程中执行完一个任务,再执行下一个任务,按照1>2>3顺序执行,遵循 FIFO 原则。

- (void)testMainQueueSyncExecution {
    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 第一个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第二个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
    });
    
    // 第三个任务
    dispatch_async(queue, ^{
        // 这里线程暂停2秒,模拟一般的任务的耗时操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
    });
    
    NSLog(@"----end-----当前线程---%@", [NSThread currentThread]);
}

四、GCD 线程之间的通讯

- (void)communication {
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        // 异步追加任务 1
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
        });
    });
}

五、GCD 的其他方法

1、dispatch_after

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

});

需要注意的是,使用 dispatch_after 实现延迟执行某动作,时间并不是很精确,因为 main dishpatch queue 在主线程的 runLoop 中执行,所以比如在每隔1/60秒执行的 RunLoop 中,block 最快在三秒后执行,最慢在3秒+1/60秒后执行,如果在 main dishpatch queue 有大量任务处理会使主线程本身的任务处理有延迟时,这个时间会增加。
如果对时间的精确度没有高要求,只是为了推迟执行,那么使用dispatch_after还是很不错的。

  • NSObject中提供的线程延迟方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
  • 通过 NSTimer 来延迟线程执行
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];

2、dispatch_once
一般我们会利用 dispatch_once 创建单例

Paste_Image.png

Paste_Image.png

从上面代码中可以看出
第一个参数 predicate,该参数是检查后面第二个参数所代表的代码块是否被调用的谓词,
第二个参数则是在整个应用程序中只会被调用一次的代码块。dispach_once 函数中的代码块只会被执行一次,而且还是线程安全的。

3、dispatch_apply


Paste_Image.png

从上面代码中可以看出,这些迭代是并发执行的和普通 for 循环一样,dispatch_applydispatch_apply_f 函数也是在所有迭代完成之后才会返回,因此这两个函数会阻塞当前线程,主线程中调用这两个函数必须小心,可能会阻止事件处理循环并无法响应用户事件。所以如果循环代码需要一定的时间执行,可以考虑在另一个线程中调用这两个函数。如果你传递的参数是串行 queue,而且正是执行当前代码的 queue,就会产生死锁。

4、dispatch_group_t dispatch_group_notify
可以使用 dispatch_group_async 函数将多个任务关联到一个 dispatch group 和相应的 queue 中,group 会并发地同时执行这些任务。而且 dispatch group 可以用来阻塞一个线程,直到 group 关联的所有的任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。

Paste_Image.png

5、dispatch_barrier_async
在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 dispatch_barrier_async 函数将任务加入到并行队列之后,任务会在前面任务全部执行完成之后执行,任务执行过程中,其他任务无法执行,直到 barrier 任务执行完成。

有时候我们会需要这样的一个场景,A任务和B任务执行完毕之后,在执行C任务,需要借助 dispatch_barrier_async 这个函数。

Paste_Image.png

Paste_Image.png

从代码中可以看出确实只有在前面A、B任务完成后,barrier 任务才能执行,最后才能执行C任务。
注意:
使用dispatch_barrier_async,该函数只能搭配自定义并行队列dispatch_queue_t使用。不能使用:dispatch_get_global_queue,否则dispatch_barrier_async的作用会和dispatch_async的作用一模一样。

6、信号量
个人理解,在多线程下使用信号量可以控制多线程的并发数目。
创建信号量,可以设置信号量的资源数。0 表示没有资源,调用 dispatch_semaphore_wait 会立即等待。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

通知信号,如果等待线程被唤醒则返回非0,否则返回0。

dispatch_semaphore_signal(semaphore);

比如,执行10个任务,然后等待2秒,然后继续执行。


Paste_Image.png

六、Dispatch Semaphore 线程同步
有时候会遇到这样的需求:
异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworkingAFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return tasks;
}

利用 Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务;

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

推荐阅读更多精彩内容