Objective-C高级编程之GCD篇

多线程编程是一项非常重要的技术,目前在iOS开发中比较流行的多线程方案是GCD和NSOperationQueue,本文将详细介绍如何使用GCD进行多线程编程。根据苹果的文档,GCD(Grand Central Dispatch)是一项提供了管理并发和异步执行任务的技术,开发者只需要将想要执行的任务追加到适当的dispatch queue中,GCD会为此生成必要的线程来执行任务。GCD使用非常简洁的语法实现了复杂的多线程编程方案,接下来我们深入认识它。

dispatch queue

GCD使用队列来实现多个任务的执行,队列分为两种,serial dispatch queue和concurrent dispatch queue,前者指队列中的任务会使用单一的线程逐个执行,而后者指队列中的任务会使用多个线程并行执行。我们可以自行创建这两种队列,方法如下:

dispatch_queue_t serialQueue = dispatch_queue_create(@"cn.test.serial.queue", NULL);
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create函数用于创建一个队列,方法的第一个参数为队列的标签(可为空,但并不推荐这样做),第二个参数指定队列的类型,当指定为NULL时即指serial dispatch queue,指定为DISPATCH_QUEUE_CONCURRENT时为concurrent dispatch queue,需要注意的是iOS6之后,GCD已经纳入了ARC的管理范围,所以不再需要手动调用相关retain/release方法。

事实上,苹果为我们提供了默认的dispatch queue,供我们使用,因此自行创建dispatch queue并不总是必要的。默认的队列有两种main dispatch queue和global dispatch queue,前者队列中的任务会在主线程中去执行,主线程只有一个,所以它是serial dispatch queue,而后者则属于concurrent dispatch queue,队列中的任务会并发执行,此队列有四种优先级可供选择,分别为DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND ,使用默认队列的方式是这样的:

//获取main dispatch queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//获取global dispatch queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_get_global_queue函数的第一个参数指定队列优先级,第二个参数默认写0即可

dispatch_async

我们使用dispatch_async向某个队列追加一项任务,async表明该方式是异步的,不会阻塞当前线程,使用方式如下

dispatch_async(serialQueue, ^ {
        NSLog(@"block");
 });

该函数第一个参数指定向哪个队列追加任务,第二个参数是一个block,表示要执行的任务。

dispatch_set_target_queue

该函数有两个功能,第一,可以指定某个队列的优先级和目标队列优先级一致,例如

dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(concurrentQueue, globalQueue);

我们创建的队列都是默认优先级,上述代码表示指定concurrentQueue优先级和globalQueue优先级一致。
第二,可用于改变队列的执行层次。举个例子,现有A,B, C三个serial dispatch queue,如果指定这三个队列的target queue为D serial dispatch queue,那么原本会并行执行的三个队列在D上就会串行执行,示例如下:

    dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue",NULL);  
    dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);  
    dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);  
    dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);  
      
    dispatch_set_target_queue(queue1, targetQueue);  
    dispatch_set_target_queue(queue2, targetQueue);  
    dispatch_set_target_queue(queue3, targetQueue);  
      
    dispatch_async(queue1, ^{  
        NSLog(@"1 in");  
        [NSThread sleepForTimeInterval:3.f];  
        NSLog(@"1 out");  
    });  
    dispatch_async(queue2, ^{  
        NSLog(@"2 in");  
        [NSThread sleepForTimeInterval:2.f];  
        NSLog(@"2 out");  
    });  
    dispatch_async(queue3, ^{  
        NSLog(@"3 in");  
        [NSThread sleepForTimeInterval:1.f];  
        NSLog(@"3 out");  
    });  

//代码执行结果
 1 in  
 1 out  
 2 in  
 2 out  
 3 in  
 3 out  

dispatch_after

该函数用于延时调用,例如当我们想要延时3秒执行某项任务时:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"test");
    });

其中第一个参数是时间,类型为dispatch_time_t,上例中直接使用dispatch_time函数生成,表示从现在开始3秒之后的时间,第二个参数是要在哪个队列中执行,上例为main dispatch queue,第三个参数为要执行的block,该函数实际上是在3秒之后向队列追加了一个block任务。

dispatch_group

当我们有多个线程在同时执行任务,我们希望在所有线程中的任务执行完毕后做某项处理,那么就可以将前面的多个线程加入到一个group中,group会监控线程中的任务是否执行完毕,执行完毕可以发送通知,之后我们就可以做某项处理。示例如下:

    dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("cn.test.serial.queue2", NULL);
    dispatch_queue_t serialQueue3 = dispatch_queue_create("cn.test.serial.queue3", NULL);
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, serialQueue1, ^ {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"test");
    });
    dispatch_group_async(group, serialQueue2, ^ {
        [NSThread sleepForTimeInterval:3];
        NSLog(@"test2");
    });
    dispatch_group_async(group, serialQueue3, ^ {
        [NSThread sleepForTimeInterval:1];
        NSLog(@"test3");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^ {
        NSLog(@"finished");
    });

//代码执行结果
test3
test
test2
finished

dispatch_group_notify函数会在group中所有线程中的任务执行完毕后被调用,因此上例不论前面线程执行谁先谁后,最后执行的一定是主线程中的block。例如有时候我们会通过多线程的方式去下载一些资源,数据等,然后所有下载完毕后在主线程中进行UI更新,就可以这样用。
除了使用dispatch_group_notify来等待group中所有任务执行完毕外,还可以通过dispatch_group_wait函数来实现同样效果

    long ret = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    if (ret == 0) {
        NSLog(@"finished");
    }else
    {
        NSLog(@"unfinished");
    }

需要说明的是该函数第二个参数是指等待时间,wait函数会使当前线程停止,等待直到:

  1. 在等待时间内,group中所有任务执行完毕,返回0
  2. 在等待时间内,group中任务没有执行完毕,超时返回非0

如上例所示,当设置时间为DISPATCH_TIME_FOREVER意味着永久等待,直到任务执行完毕。

dispatch_barrier_async

当我们需要多个线程对某个属性进行频繁的读写操作时,如果不对线程加以控制,很容易造成读写混乱,因此我们可能希望:当进行读操作时,可以多个线程同时读取以保证高效率,而进行写操作时,不可以有任何线程进行读操作,写入完毕,线程又可以并发读取属性值,这种情况barrier技术正好可以方便的解决问题,示例如下

    dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^ { /*读取*/ });
    dispatch_async(concurrentQueue, ^ { /*读取*/ });
    dispatch_async(concurrentQueue, ^ { /*读取*/ });
    dispatch_barrier_async(concurrentQueue, ^ { /*写入*/});
    dispatch_async(concurrentQueue, ^ { /*读取*/ });
    dispatch_async(concurrentQueue, ^ { /*读取*/ });

dispatch_sync

有dispatch_async,当然也就有dispatch_sync,该函数表示同步执行,即当前线程会等待sync中的任务执行完毕才继续往下执行,例如

    dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
    dispatch_sync(serialQueue1, ^ {
        NSLog(@"test");
    });

在主线程中执行上述代码,当执行到dispatch_sync时,线程会进行等待,直到追加到serialQueue1中的block执行完毕,主线程才继续往下执行。这种同步调用如果不加注意,就容易造成线程死锁问题,例如在主线程中执行下述代码就会造成死锁问题,主线程停止,等待lock执行完毕,而追加到主线程的block因为主线程停止而永远不会被执行,于是产生死锁。

    dispatch_sync(dispatch_get_main_queue(), ^ {
        NSLog(@"test");
    });

dispatch_apply

该函数与dispatch_sync有些关联,是指重复向某个队列追加block并等待全部block执行完毕后返回。例如当我们想遍历某个数组,对所有元素做些操作,可能会使用循环的方式,当数组非常大的时候,我们可能希望使用多线程去遍历数组所有元素来提高效率,那么就可以使用dispatch_apply,示例如下

    NSArray *array = /*数组赋值*/;
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_apply([array count], concurrentQueue, ^(size_t index) {
        NSLog(@"array item: %@", [array objectAtIndex:index]);
    });

dispatch_apply的第一个参数指重复追加的次数,第二个为执行的队列,第三个为要执行的block,和前面不同的是,block里有一个参数,这个参数用来标识追加到队列中的每个block。上述代码中所有block在执行完毕后才能执行后续代码。

dispatch_suspend/dispatch_resume

这一对函数非常简单,dispatch_suspend用于挂起队列,之后队列中所有任务都会暂停执行,dispatch_resume使队列从暂停状态恢复为继续执行状态。

dispatch semaphore

dispatch_semaphore可以实现更加精细化地对线程进行管理。semaphore是拥有计数的信号量,我们可以通过semaphore对多线程访问共享资源时进行精细化的排他控制,也可以通过semaphore实现并发线程数量的控制。
为了便于理解,我们举一个停车的例子,假设停车场只剩下两个车位,这时候同时来了三辆车,那么势必只能开入两辆车,另外一辆需要先一旁等待,直到有车离开停车位,等待的车才能开入,我们用信号量来实现这段逻辑:

    dispatch_semaphore_t sema = dispatch_semaphore_create(2);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(globalQueue, ^ {
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"into 1");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"out 1");
        dispatch_semaphore_signal(sema);
    });
    dispatch_async(globalQueue, ^ {
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"into 2");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"out 2");
        dispatch_semaphore_signal(sema);
    });
    dispatch_async(globalQueue, ^ {
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"into 3");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"out 3");
        dispatch_semaphore_signal(sema);
    });

//代码执行结果
2017-06-05 12:32:57.337284+0800 Demo[16270:3721871] into 1
2017-06-05 12:32:57.337416+0800 Demo[16270:3721882] into 2
2017-06-05 12:32:58.341023+0800 Demo[16270:3721882] out 2
2017-06-05 12:32:58.341199+0800 Demo[16270:3721871] out 1
2017-06-05 12:32:58.341402+0800 Demo[16270:3721870] into 3
2017-06-05 12:32:59.345365+0800 Demo[16270:3721870] out 3

操作信号量的主要有三个函数:
dispatch_semaphore_create用来创建信号量,参数表示初始的信号总量。
dispatch_semaphore_wait表示等待信号量,每执行一次信号量减1,第二个参数是指等待时间,DISPATCH_TIME_FOREVER意味着永久等待,该函数会使当前线程处于等待状态,直到以下两种情况才会返回:

  1. 在等待时间内,信号量大于等于1,这时wait函数返回0,信号量减1。
  2. 超出了等待时间,信号量依旧不满足大于等于1,wait函数因超时返回非0。

dispatch_semaphore_signal表示发送信号量,每执行一次信号量加1
我们也可以使用信号量对并发线程数量进行控制,示例如下:

    dispatch_semaphore_t sema = dispatch_semaphore_create(10);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    for (int i = 0; i < 100; i++) {
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        dispatch_async(globalQueue, ^ {
            NSLog(@"test: %d", i);
            [NSThread sleepForTimeInterval:1];
            dispatch_semaphore_signal(sema);
        });
    }

该例可以这样理解,信号量初始为10,for循环在创建了10个线程后,信号量减为0,于是for循环就进行等待,直到某一个线程结束,增加了一个新的信号量,才能继续执行,这样便控制了并发数量不超过10个。

dispatch_once

该函数比较简单,常常用于单例的生成,它表示其代码块在程序中只会执行一次,用法如下:

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"some init...");
    });

虽然使用@synchronized也能保证单例的线程安全,但dispatch_once性能要远高于@synchronized,因此单例创建推荐使用dispatch_once

dispatch I/O

dispatch I/O是指使用多线程进行文件读取的技术。主要思想是当文件比较大时,我们可以将文件分段,然后使用多线程进行读取,再合并,这种方式可以大大提高文件读取速度。

dispatch source

dispatch source是指在内核发生各种事件时,开发者可以执行自定义处理的技术。内核中事件的类型有多种,最常见的一种是定时器事件,使用示例如下:

    dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue1);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"test");
    });
    dispatch_resume(timer);

上例创建了一个定时器,每两秒执行一次block块。dispatch_source_set_event_handler函数设置了每次定时器事件触发时执行的处理,通过dispatch_resume启动该定时器。
当需要取消该定时器时,可以通过dispatch_source_cancel取消,另外还可以指定取消时要执行的处理。

    dispatch_source_cancel(timer);
    dispatch_source_set_cancel_handler(timer, ^ {
        NSLog(@"cancel");
    });

最后

GCD是基于内核级别的实现,在性能上是非常优异的,而且它语法简洁,是一套非常好的多线程编程方案。

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

推荐阅读更多精彩内容