iOS多线程编程之GCD

  • 本文的写作目的是为学习记录,同时分享给大家,希望大神能够对文中错误的理解进行指正。
  • 如果文章内容涉及到其他已经发表了,但文章中又未提及转载事项,请及时与本人联系。
  • 本文为个人理解,如果部分知识点与真实情况有出入,请忽略本文。

1 iOS和OS X中多线程基本概念

1.1 了解GCD

1.1.1 iOS和OS X中,实现多线程编程的方式。

  • NSThread,调用方便,但需要手动管理线程生命周期,功能较简单,适用于简单实现多线程需求。
  • NSOperation & NSOperationQueue,苹果官方对GCD的封装,完全面向对象,很便利地实现GCD的大部分功能。很多大神建议,在NSOperation能实现的情况下,尽量用NSOperation而不是GCD,因为方便、代码可读性(帮你写好的代码,干嘛不用)。本文不予讨论。
  • GCD,苹果公司为多核的并行运算提出的解决方案。基于C语言,但不需要做额外的处理即能够直接使用。很强大的多线程处理方式。
  • Pthreads,可在多个操作系统使用的,手动管理线程生命周期的,基于C语言的,多线程技术。简单来说就是移植性强,但是用起来复杂。

1.1.2 GCD(Grand Central Dispatch)

在iOS和OS X中实现异步执行任务(多线程管理)的技术之一。不需要手动管理线程的生命周期,功能强大、使用灵活,能满足你对多线程管理的所有需求。
例如:

dispatch_async(queue, ^{
//在此处编写需要副线程处理的代码(后台执行)
    dispatch_async(dispatch_get_main_queue( ), ^{
        //反馈到主线程,处理副线程执行完成的结果(反馈到主线程执行)
    });
});

1.2线程与进程

1.2.1 线程

一条无分叉路径的、线性的、需要CPU分配一个核心去执行的命令。

Tips:在iOS和OS X中,每个线程系统都会分配一个RunLoop对象(需要手动获取),该对象可以实现循环执行该线程。

1.2.2 进程

并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。

1.2.3 两者的关系

  • 系统分配一个进程去管理一个程序的运行。
  • 程序中可能涉及一个或多个线程同时执行,此时就需要进程实时分配CPU、内存等计算机资源。

Tips:在计算机处理多线程时,有时会通过定时切换执行任务(每隔一定时间,来回切换执行的任务),来实现CPU单个核心同时执行多个线程。

Tips:从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列,这被称为“上下文切换”。

1.3 线程队列

1.3.1 并行

  • 对n个需要同时执行(不等待现在执行中的任务处理结束,直接执行下一任务)的任务创建线程队列,是一种多线程技术。
  • 虽然可以添加多个并行线程队列,但在(但不仅限于)iOS和OS X系统下,系统会根据当前线程数量、计算机性能等因素,来判断当前需要同时执行的任务数量(就是说,不管你一下子添加多少个并行的任务,同时执行的任务数量是有上限的)。

1.3.2 串行

  • 对n个需要逐一执行(等待现在执行中的任务处理结束后,才执行下一任务)的任务创建线程队列,是一种多线程技术。
  • 异步的添加过多并行线程队列,系统会为每个队列创建一个线程,而且不会有数量限制。
  • 所以,如果过多创建串行队列,就会消耗大量系统资源,降低性能。

1.3.3 并行与并发

并发,是一种现象(可以说是一个问题);
而并行,是一种多线程的技术(可以说是解决问题的方案)。
例如,并发就像有n个人排队买票,而并行,则像是同时开了m个售票窗口解决n个人同时买票的耗时问题。

1.4 同步与异步

1.4.1 同步

  • 同步的,将需要执行的任务,追加到指定线程队列中。
  • 当需要执行的任务,同步追加到执行线程队列时,指定线程队列必须暂停,等待该任务执行完毕,才能继续往下执行。
  • 所以同步线程,容易引发死锁问题。

1.4.2 异步

  • 非同步的,将需要执行的任务,追加到指定线程队列中。
  • 将需要执行的任务,非同步的追加到指定线程队列后,会形成一个新的线程队列,而因为非同步添加,所以系统会新生成n个线程来执行该新的线程队列。
  • 因为执行时间不同步,所以容易引发数据竞争问题。

1.5 多线程问题

虽然使用多线程编程可以保证用户界面的响应,但如果使用不当容易发生各种编程问题。以下是常见的三种:

1.5.1 数据竞争

多个线程同时执行时,更新相同的资源,导致数据的不一致。

1.5.2 死锁

多个线程同时执行时,如果相互之间限制执行,会导致多个线程相互持续等待而使程序无法继续执行。

1.5.3 大量消耗资源

使用过多线程会消耗大量计算机资源。

2 iOS和OS X中GCD的API

2.1 线程队列(Dispatch Queue)

Dispatch Queue,执行处理的等待队列。
开发者只需将需要执行的任务添加到合适的线程队列,即可实现多线程编程。

2.1.1并行队列(DISPATCH_QUEUE_CONCURRENT)

先进先出(FIFO First-In-First-Out)的顺序执行处理,并且不等待现在执行中的处理结束。
其他释义同1.3.1。

2.1.2串行队列(DISPATCH_QUEUE_SERIAL)

先进先出(FIFO First-In-First-Out)的顺序执行处理,并且等待现在执行中的处理结束。
其他释义同1.3.2。

2.2 创建线程队列(dispatch_queue_create函数与dispatch_queue_t类型变量)

2.2.1释义

dispatch_queue_create是根据参数类型,创建线程队列的函数。
dispatch_queue_t为dispatch_queue_create函数的返回值类型,线程队列类型。

2.2.2 用法:

dispatch_queue_t testQueue = dispatch_queue_create("com.Test.GCD.testQueue",DISPATCH_QUEUE_CONCURRENT); 
  • 参数一,生成线程的名称(推荐使用程序ID的逆序全程域名+变量名称);
  • 参数二,生成线程类型,串行为DISPATCH_QUEUE_SERIAL(或设为NULL),并行指定为DISPATCH_QUEUE_CONCURRENT。
  • 返回值,dispatch_queue_t类型线程队列变量。

2.2.3 注意

在iOS 6.0 SDK以前版本,GCD中所有create的创建的对象,都需要用dispatch_retain、dispatch_release来进行内存管理。而iOS 6.0 SDK以后的版本,只要开启ARC就不需要对GCD的对象进行内存管理。

2.3 异步追加(dispatch_async函数)

2.3.1释义

  • 异步追加线程队列,线程队列按照追加的顺序(先进先出)执行处理。
  • 其他释义同1.4.1。
  • 将需要异步执行的代码写在Block中, Block会异步的添加到线程队列中。。
  • Block的内容,可以参照此文

2.3.2 用法

//创建一个并行队列testQueue
dispatch_queue_t testQueue = dispatch_queue_create("com.Test.GCD.testQueue",DISPATCH_QUEUE_CONCURRENT);

//将Block中需要执行的任务,异步的追加到testQueue中
dispatch_async(testQueue, ^{
    //需要执行的任务
}); 

2.4 主线程(Main Dispatch Queue)

2.4.1释义

当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。(此段释义来自百度百科)

在iOS中,所有追加到主线程的处理,都是在主线程的RunLoop中执行。
当在后台执行的任务完成后,如果需要将结果在UI中展示,此时则需要调用主线程来更新用户界面。

2.4.2 用法

dispatch_queue_t testQueue = dispatch_queue_create("com.Test.GCD.testQueue",DISPATCH_QUEUE_CONCURRENT);

dispatch_async(testQueue, ^{
    //需要执行的任务
    dispatch_queue_async(dispatch_get_main_queue(), ^{
        //将结果反馈到主线程,例如更新用户界面
    });
}); 

2.5 全局队列(Global Dispatch Queue)

2.5.1释义

系统提供的并行线程队列(DISPATCH_QUEUE_CONCURRENT),不需要特意生成(推荐使用)。
根据已获取并行队列的优先级,系统会自动判断执行的先后,但由于执行时长的不确定性,并不能保证完全按照优先级来执行。

2.5.2 Global Dispatch Queue的4个优先级:

  • DISPATCH_QUEUE_PRIORITY_HIGH,最高优先级;
  • DISPATCH_QUEUE_PRIORITY_DEFAULT,默认优先级;
  • DISPATCH_QUEUE_PRIORITY_LOW,低优先级;
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND,后台优先级(在最高优先级队列和已经追加的后台优先级队列之后,执行)。

2.5.3 用法:

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

2.6 优先级设定(dispatch_set_target_queue函数)

2.6.1释义

dispatch_queue_create函数创建的线程队列,不论是并行还是串行,其优先级皆为默认优先级。
如果需要变更线程队列的优先级,需要用到dispatch_set_target_queue函数。

2.6.2 用法

//创建串行线程队列testQueue
dispatch_queue_t testQueue = dispatch_queue_create(“com.Test.GCD.testQueue”, NULL); 

//获取后台优先级的全局队列
dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

//将testQueue的优先级设定成与testGlobalQueue相同
dispatch_set_target_queue(testQueue, testGlobalQueue);
  • 参数一:变更值,需要变更优先级的线程队列;
  • 参数二:目标值,将参数一的优先级变更成与参数二相同。

2.5.3 注意

  • 参数一不能为Main Dispatch Queue或Global Dispatch Queue。
  • 参数二如果为串行线程队列,那么参数一与参数二将串行执行任务(原本俩货是并行的,但经过dispatch_set_target_queue之后就变成串行,而且参数二的队列优先执行)。

2.7 线程计时(dispatch_after函数与dispatch_time_t类型变量)

2.7.1释义

dispatch_after函数可以将Block任务,在指定时间之后执行。
dispatch_time_t类型声明一个时间类型变量。

2.7.2 用法:

//创建一个dispatch_time_t类型的时长变量
dispatch_time_t testTime = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

//在主线程中,延后最少3秒执行Block中的任务(为什么最少3秒,后面会讲)
dispatch_after(testTime, dispatch_get_main_queue(), ^{
    NSLog(@”延后最少3秒,再执行!”);
});
  • dispatch_time函数用来创建一个时间,返回一个以参数一为开始时间,经过参数二设置的、以毫秒为单位的时长之后的一个dispatch_time_t时间类型变量(有点绕口,简单说就是参数一的时间加上参数二的时长,得出的值)。
  • 获取dispatc_time_t类型变量,还有一种方法,用dispatch_walltime函数来获取,感兴趣的朋友可以自行查阅资料。
  • dispatch_after函数的用法,一目了然。

2.7.3 注意

  • dispatch_after函数并非是在testTime后就立马执行处理,而是在testTime追加Block到线程队列中。如果是在主线程,则会追加到RunLoop中。
  • 因此Block的执行时间是大于等于testTime,且小于RunLoop循环间隔时长+ testTime。

2.8 线程群(Dispatch Group)

2.8.1释义

当一个任务,需要等并行的多个任务全部执行完毕之后执行该任务。这时就需要用到线程群(Dispatch Group)来解决这个问题。可以将多个并行任务都添加到线程群中,等线程群中的所有任务都执行完毕后,再指定去执行该任务。

2.8.2 用法

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建线程群
dispatch_group_t testGroup = dispatch_group_create();

//追加任务到线程队列,再讲线程队列追加到线程群
dispatch_group_async(testGroup, testGlobalQueue, ^{NSLog(@”并行任务一”);}); 
dispatch_group_async(testGroup, testGlobalQueue, ^{NSLog(@”并行任务二”);});
dispatch_group_async(testGroup, testGlobalQueue, ^{NSLog(@”并行任务三”);});

//当线程群中的任务都执行完毕,将所有结果反馈到对应线程
dispatch_group_notify(testGroup, dispatch_get_main_queue(), ^{
    NSLog(@”反馈以上三个任务的结果”);
});

//该函数不对结果做处理,仅等待线程群执行结束
//dispatch_group_wait(testGroup, DISPATCH_TIME_FOREVER);

执行结果:

并行任务二
并行任务三
并行任务一
反馈以上三个任务的结果

2.8.3 注意

  • 线程群中可以追加并行队列,也可以追加串行队列,只要在线程群中的任务全部执行完毕,dispatch_group_notify函数就能检测到。
  • dispatch_group_notify函数可以在线程群中的任务全部执行完毕之后,做出相应的反馈;也可以用dispatch_group_wait函数仅等待全部任务执行完毕,而不作处理。

2.9 线程等待(dispatch_barrier_async函数)

2.8.1 释义

当程序执行到dispatch_barrier_async函数时,会等待在此之前的线程队列中的任务执行完毕,然后暂停线程队列,当dispatch_barrier_async函数中Block执行结束,再继续执行线程队列的任务。

2.9.2 用法:

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务一”);}); 
dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务二”);}); 
dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务三”);}); 

dispatch_barrier_asyn (testGlobalQueue, ^{NSLog(@”任务一、二、三已经执行完成,任务四、五、六还未开始执行”);}); 

dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务四”);}); 
dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务五”);}); 
dispatch_async(testGlobalQueue, ^{NSLog(@”并行任务六”);}); 

2.9.3 注意

当多个并行任务需要对同一数据进行写入操作,而且之后又有多个并行任务需要对该数据进行读取操作时。可能由于同一时间对该数据进行操作,而导致数据竞争问题;也很有可能后面执行的任务读取到的数据并不一致问题。将写入操作集中到dispatch_barrier_async函数处理,就能解决这样的问题。

2.10 同步追加(dispatch_sync)

2.10.1释义

同步追加线程队列,线程队列按照追加的顺序(先进先出)执行处理。
其他释义同1.4.1。

2.10.2 用法:

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_sync(testGlobalQueue, ^{
    //需要执行的任务
}); 

2.10.3 注意

  • 一旦调用dispatch_sync函数,那么在Block处理结束之前,函数不会返回。因此容易造成线程死锁。

例如:

dispatch_queue_t testQueue = dispatch_queue_create(“com.Test.GCD.testQueue”, NULL);


    dispatch_sync(testQueue, ^{
        NSLog(@”会执行!”);

    //由于testQueue串行队列,所以Block等待当前线程队列执行完毕,在执行Block
    //而由于dispatch_sync同步追加,会让当前线程队列等待Block执行完毕,再恢复
    //由此相互等待,则造成线程死锁
    dispatch_sync(testQueue, ^{
        NSLog(@”此段代码不会执行!”);
    }); 
}); 
  • 由于主线程(Main Dispatch Queue)也是同步线程,所以上面的testQueue换成主线程也会引起死锁。

2.11 多次追加(dispatch_apply函数)

2.11.1 释义

该函数按指定的次数将指定的Block追加到指定的线程队列中,并等待全部处理执行结束

2.11.2 用法

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(10, testGlobalQueue, ^(size_t index) {
    NSLog(@”%zu”,index);
});
NSLog(@”完成!”);

2.12 线程挂起(dispatch_suspend函数)与线程恢复(dispatch_resume函数)

2.12.1 释义

dispatch_suspend函数能够暂停指定线程的执行。
dispatch_resume函数恢复执行线程的执行。

2.12.2 用法

//暂停
dispatch_suspend(testQueue);
//恢复
dispatch_resume(testQueue);

2.13 线程计数(Dispatch Semaphore)

2.13.1 释义

Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。
当指定线程计数为0时则暂停;大于等于1时,减去1而恢复。

2.13.2 用法

dispatch_queue_t testGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray * array = [[NSMutableArray alloc] init];

//创建一个初始值为1的线程计数
dispatch_semaphore_t testSemaphore = dispatch_semaphore_create(1);

for (int i = 0; i < 1000; ++i) {

    dispatch_async(testGlobalQueue, ^{
    
        //计数-1,暂停线程,直到线程计数大于等于1
        dispatch_semaphore_wait(testSemaphore, DISPATCH_TIME_FOREVER);
    
        //线程安全,不存在数据竞争
        [array addObject:[NSNumber numberWithInt:i]];
    
        //计数+1,恢复线程
        dispatch_semaphore_signal(testSemaphore);
    });
}

2.14 单例模式(dispatch_once函数)

2.14.1 释义

通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。(此段释义来自百度百科)
dispatch_once函数可以保证在应用程序执行中,只执行一次指定任务。

2.14.2 用法

static dispatch_once_t once;
dispatch_once(&once, ^{
    //需要进行的处理
});

2.14.3 注意

以上用法仅为单例模式的其中一种写法,其他写法及深入的探讨,请自行查阅资料,这里就不展开了。

参考
《Objective-C高级编程 iOS与OS X多线程和内存管理》【日】Kazuki Sakamoto Tomohiko Furumoto 著 黎华 译
《关于iOS多线程,你看我就够了》 http://www.jianshu.com/p/0b0d9b1f1f19

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

推荐阅读更多精彩内容

  • 1. GCD简介 iOS开发中多线程的API主要有pthread,NSThread,NSOperation和GCD...
    安东_Ace阅读 1,203评论 0 6
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,676评论 1 17
  • 本篇博客共分以下几个模块来介绍GCD的相关内容: 多线程相关概念 多线程编程技术的优缺点比较? GCD中的三种队列...
    有梦想的老伯伯阅读 995评论 0 4
  • 1. GCD简介 什么是GCD呢?我们先来看看百度百科的解释简单了解下概念 引自百度百科:Grand Centra...
    千寻_544f阅读 340评论 0 0
  • 一·步步为营,步步为赢 我记得第一次走进学校自习室的时候,我是不知所措的。站在自习室中间环顾四周,教室安静得脚落在...
    十柒0602阅读 421评论 9 13