线程和进程,以及GCD的简单使用
文章目录
- 进程(Process)和线程(Thread)
- 进程和线程的区别
- 线程的用途
- 并发和并行
- 多线程带来的问题
- Grand Central Dispatch的使用
- 串行队列
- 并发队列
- 创建和管理Dispatch Queues
- 获取主队列
- 获取全局队列
- 自定义队列
- 向队列中添加任务
- 队列组
- 其他方法
进程(Process)和线程(Thread)
现代的操作系统可以同时运行多个程序。因此,我们可以在浏览器(一个程序)阅读这篇文章,同时在音乐播放器(另一个程序)上听音乐
每个程序都被称为正在执行的进程,操作系统拥有协调进程同时运行的能力,利用底层硬件的技巧。最终,我们会感觉程序同时在运行
同时执行多个操作不一定同时运行多个进程,因为每个进程都可以在其内部同时运行多个子任务(sub-task),每个子任务称为线程
我们可以将线程视为进程本身的一部分,每个进程启动时都会触发一个线程,通常称为主线程(main thread、primary thread)。随后,根据程序、开发人员需要启动、终止其他线程
可以把操作系统视为包含多个进程的容器,进程是包含多个线程的容器
进程和线程的区别
操作系统为每个进程分配一块内存。默认情况下,进程间无法共享数据,除非采用更为高级的技巧,即进程间通信
与进程不同,线程之间共享操作系统分配给其父进程的内存块。音频播放引擎可以轻松访问音乐播放器主界面中的数据,反之亦然。因此,线程之间可以很方便的进行通信。线程更为轻量级、占用资源少、创建速度快。因此,线程也被称为轻量级的进程
线程可以很方便的使程序同时执行多项任务。如果没有线程,则程序一次只能执行一项任务,在进程执行完毕后与系统进行同步。这将更加复杂和缓慢
线程的用途
一个进程内使用多个线程可以并发处理任务,提高处理速度
但有以下三点需要考虑:
- 不是每一个程序都需要多线程。如果 app 执行顺序任务,或需要等待用户输入,多线程可能没有太大好处。
- 不是程序线程越多,其速度就会越快。每个子任务都必须经过仔细考虑和设计,以并行执行。
- 并发的多线程并不能保证会并行处理,具体是否会并行需要根据硬件、当前状态来确定。
有一点需要注意,如果设备不支持同时执行多种操作,则操作系统必须伪造多任务并行
并行(parallelism)是任一时间多任务同时运行,并发(concurrency)指多任务同时发生,但未必会被同时执行。
伪造并行是通过上下文切换,制造一种同时运行的错觉
并发和并行
中央处理器负责运行程序。CPU 由多个部分组成,主要部分是所谓的核心(Core),即实际执行计算的地方。任一时间单个 Core 只能执行一项任务
任一时间单个 Core 只能执行一项任务限制了程序的运行。为此,操作系统开发了高级技术,即便是单核的设备,也能提供同时运行多个进程、线程的能力。其中最重要的一项技术是抢占式多任务处理(Preemptive Multitasking),在抢占式环境下,操作系统完全决定进度调度方案,操作系统可以剥夺任务的时间片,提供给其他任务,后续再恢复执行暂停的任务
如果 CPU 仅具有一个内核,操作系统的任务之一就是将单个内核的计算资源分布给多个进程、线程,这些进程、线程将在一个循环中依次执行。这样会产生有多个程序或一个程序多项任务同时运行的假象,但此时并未实现真正的并行
现在,CPU 一般都是多核,每个内核一次可以执行一项操作,也就是 CPU 具有两核及以上才能实现真正的并行
多线程带来的问题
同一进程内的所有线程共享同一块内存,这样同一个进程内的线程可以很方便的交换数据
多个线程同时从同一内存区域读取数据并不会出现问题,但同时有线程写入、读取就会出现问题。可能出现以下两个问题:
- 数据争用 Data Race:也称为数据竞争,一个线程修改数据时,另一个线程正在读取数据。如果写入还没有完成,就会读取到修改一半或损坏的数据,在我的天气预报仿写项目中,就因为网络请求单例的数据没有完全写入单例的属性中,导致UI数据出现错误
- 竞争条件 Race Condition:也称为竞争冒险(Race Hazard)、竞态条件,指一个系统或进程的输出依赖不受控制的事件出现顺序或出现时机
CPU 的内核一次只能执行一个机器指令。因为其不可分割为更小操作,被称为原子的(atomic)
不可分割的特点使原子操作本质上线程安全。当有线程对共享数据执行原子写入时,其他线程无法读取,也就不会读取到损毁的数据;相反,当有线程对共享数据执行读取操作时,其读取到那一刻的值。线程无法插入到执行原子操作的指令中间,因此不会出现数据争用
这也是为什么我们会把线程安全的属性修饰符叫做atomic
Grand Central Dispatch的使用
GCD提供了一个队列,用于管理向其提交的任务,所有的dispatch queue都是先进先出的数据结构,因此队列中任务运行顺序与添加顺序一致,即第一个进入队列的任务,第一个被执行,第二个进入队列的任务第二个被执行,以此类推
所有的队列自身都是线程安全的,所以可以从多个线程中访问它们
GCD的队列分为串行队列和并发队列两种
串行队列
在串行队列中,一次只会执行一个任务,一个任务完成后另一个任务才会开始,两个任务间隔时间无法确定
串行队列中任务执行时间由GCD控制,唯一可以保证的是任务执行顺序和添加顺序一致
并发队列
GCD只能保证并发队列中的任务按照添加的顺序开始执行。至于task结束顺序,两个task间时间间隔,任一时间正在运行task数量都无法保证
上图中的Block 0开始一段时间后Block 1才开始执行,Block 1、Block 2、Block 3开始时间相差很短。虽然Block 3比Block 2开始的晚,但结束的早
GCD决定什么时间开始执行任务。如果两项任务执行时间重合,由GCD决定是否在另一个内核上(如果目前有空闲内核)运行任务,还是执行context switch来执行另一任务
创建和管理Dispatch Queues
在提交任务到dispatch queue之前,必须确定要使用的队列类型以及如何使用。如果有特殊用途,可以自定义配置队列
为了方便使用,GCD默认提供了主队列dispatch_get_main_queue
和全局队列dispatch_get_global_queue
获取主队列
主队列是一个全局的串行队列,运行在应用程序的主线程上
// 获取主队列dispatch_queue_t mainQueue = dispatch_get_main_queue();
获取全局队列
全局队列是并发队列。
系统为每个应用程序提供了四个不同优先级的全局队列。因为这些队列是全局的,可以直接使用dispatch_get_global_queue
函数请求队列,不需要显式创建
// 获取优先级为QOS_CLASS_USER_INITIATED的全局队列dispatch_queue_t aQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
dispatch_get_global_queue
函数的第一个参数指定Quality of Service(简称 QoS)
指定优先级的四个参数如下:
QoS Class | 用途 | 任务所需时间 |
---|---|---|
QOS_CLASS_USER_INTERACTIVE | 优先级与主线程任务相同,用于处理用户正在等待、需要立即反馈的任务。追求性能和响应速度。 | 接近瞬间完成。 |
QOS_CLASS_USER_INITIATED | 用于用户发起的,需要立即获得结果的任务。例如,打开磁盘上的文档,用户点击界面时执行相应操作,即用户交互的下一步需要这一步的执行结果。对响应和性能有较高要求的。 | 几乎瞬间完成,如几秒钟或更少。 |
QOS_CLASS_UTILITY | 需要一些时间来完成,不需要立即返回结果。例如,下载或导入数据。一般有提示进度的进度条。追求响应和能源效率的平衡。 | 几秒钟至几分钟。 |
QOS_CLASS_BACKGROUND | 在后台运行,不需要用户看到。例如:索引、同步、备份。关注能源效率。 | 几分钟到几个小时。 |
QOS_CLASS_USER_INTERACTIVE
的优先级与主线程相同,但QOS_CLASS_USER_INTERACTIVE
仍然是在全局队列,更新UI只能在主线程中
自定义队列
除了使用系统提供的队列,还可以手动创建队列。
// 创建串行队列dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.serialQueue", DISPATCH_QUEUE_SERIAL);// 创建并发队列dispatch_queue_t conQueue = dispatch_queue_create("com.GCD.conQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create
函数有两个参数,第一个参数指定队列名称,debugger和性能工具会显示此处队列名称以帮助跟踪任务执行情况。第二个参数指定是串行还是并发队列
我们可以创建任意数量串行队列,但这些串行队列之间是并发关系。例如,创建了四个串行队列,每个串行队列执行一个任务,系统可能同时执行这四个任务
向队列中添加任务
-
dispatch_async(queue, block)
异步 提交一个任务到指定的队列,并且立即返回,通常用于将耗时操作(如网络请求、文件读写、大数据处理)放在后台执行,以避免阻塞主线程
-
dispatch_sync(queue, block)
同步 提交一个任务到指定的队列,函数会 等待任务执行完毕 后才返回,在当前线程需要依赖另一个队列的任务执行结果时使用。但要特别注意,不要在主线程同步提交任务到主队列,会造成死锁
队列组
当一个队列中所有的任务全部执行完毕后,才会执行通知中的操作
dispatch_group_create()
:创建一个新的队列组dispatch_group_async(group, queue, block)
:向队列组中添加任务dispatch_group_notify(group, queue, block)
:在队列组中的所有任务都完成后,执行一个通知任务dispatch_group_wait(group, timeout)
:同步等待队列组中的所有任务完成
在我的天气预报项目中有使用队列组,所以我们就用这个来当例子。我们需要等待所有的城市数据全部请求到之后,再去更新UI界面
// 根据自身的城市名称,请求所有城市的网络数据
- (void) requestAllCityWeatherData {dispatch_group_t group = dispatch_group_create();for (NSString* cityName in self.cityNameArray) {dispatch_group_enter(group);NSLog(@"新的循环,城市名为%@",cityName);__weak typeof(self) weakSelf = self;// 请求网络数据// 回调操作 dispatch_group_leave(group);}dispatch_group_notify(group, dispatch_get_main_queue(), ^{for (int i = 0; i < self.allWeatherArray.count; i++) {DetailPageVC* detailVC = [[DetailPageVC alloc] init];detailVC.model.Info = self.allWeatherArray[i];detailVC.model.InfoDic = self.allWeatherDic[self.cityNameArray[i]];NSLog(@"%@",self.allWeatherDic[self.cityNameArray[i]][@"simple"]);[self.VCArray addObject:detailVC];}// NSLog(@"发布通知");[[NSNotificationCenter defaultCenter] postNotificationName:@"updateMasterPage" object:nil];// 保存数据
// [[NSUserDefaults standardUserDefaults] setObject:self.cityNameArray forKey:@"cityNameArray"];
// [[NSUserDefaults standardUserDefaults] setObject:self.allWeatherArray forKey:@"allWeatherArray"];
// [[NSUserDefaults standardUserDefaults] synchronize];});
// [[NSNotificationCenter defaultCenter] postNotificationName:@"updateMasterPage" object:nil];}
其他方法
dispatch_once(predicate, block)
确保以下的代码只会执行一次,也是这是实现单例模式最简单、最安全的方式
+ (instancetype)sharedInstance {static Singleton *instance = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[super allocWithZone:NULL] init];});return instance;
}
dispatch_barrier_async(queue, block)
向 并发队列 提交一个“栅栏”任务。这个任务必须是队列中唯一正在执行的任务,它会等待前面提交的任务都执行完毕,然后自己开始执行,并且在它执行完成前,后面提交的任务都不能开始
通常用于读写数据库或文件。你可以在多个线程中并发地“读”,但当需要“写”时,你需要一个栅栏来确保此时没有其他线程在读写,保证数据安全