iOS多线程编程之GCD

时间:2021-09-22 05:20:03

声明:

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


正文:

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

1.1 了解GCD

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

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

1.1.2 GCD(GrandCentral 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 两者的关系

(1)系统分配一个进程去管理一个程序的运行。
(2)程序中可能涉及一个或多个线程同时执行,此时就需要进程实时分配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 线程队列(DispatchQueue)

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);

(1)参数一,生成线程的名称(推荐使用程序ID的逆序全程域名+变量名称);

(2)参数二,生成线程类型,串行为DISPATCH_QUEUE_SERIAL(或设为NULL),并行指定为DISPATCH_QUEUE_CONCURRENT。
(3)返回值,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 主线程(MainDispatch 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 全局队列(GlobalDispatch 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);
 
(1)参数一:变更值,需要变更优先级的线程队列;
(2)参数二:目标值,将参数一的优先级变更成与参数二相同。

2.5.3 注意

(1)参数一不能为MainDispatch Queue或Global Dispatch Queue。
(2)参数二如果为串行线程队列,那么参数一与参数二将串行执行任务(原本俩货是并行的,但经过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秒,参照2.7.3)
dispatch_after(testTime,dispatch_get_main_queue(), ^{
       NSLog(@"延后最少3秒,再执行!");
}
(1)dispatch_time函数用来创建一个时间,返回一个以参数一为开始时间,经过参数二设置的、以毫秒为单位的时长之后的一个dispatch_time_t时间类型变量(有点绕口,简单说就是参数一的时间加上参数二的时长,得出的值)。
(2)获取dispatc_time_t类型变量,还有一种方法,用dispatch_walltime函数来获取,感兴趣的朋友可以自行查阅资料。
(3)dispatch_after函数的用法,一目了然。

2.7.3 注意

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

2.8 线程群(Dispatch Group)

2.8.1释义

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

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 注意

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

2.9 线程等待(dispatch_barrier_async函数)

2.9.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 注意

(1)一旦调用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(@"此段代码不会执行!");
	});
});
(2)由于主线程(MainDispatch 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_tindex) {
       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 线程计数(DispatchSemaphore)

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 = [[NSMutableArrayalloc] 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);
             
              //线程安全,不存在数据竞争
              [arrayaddObject:[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