文章目錄
  1. 1. 1.多线程基本概念
    1. 1.1. 1.1 多线程编程的基本概念:
    2. 1.2. 1.2 iOS多线程实现方案对比
    3. 1.3. 1.3 NSThread的使用
    4. 1.4. 1.4 GCD 的使用
    5. 1.5. 1.4.1 GCD 的队列类型
    6. 1.6. 1.4.2 GCD 任务组 (dispatch_group)
    7. 1.7. 1.4.3 任务派发函数
    8. 1.8. 1.4.4 GCD 停止和恢复
    9. 1.9. 1.4.5 GCD 其他方法
    10. 1.10. 1.5 NSOperation 的使用
    11. 1.11. 1.5.1 创建操作
    12. 1.12. 1.5.2 创建操作队列
    13. 1.13. 1.5.3 设置操作队列属性
    14. 1.14. 1.5.4 将操作添加到操作队列
    15. 1.15. 1.5.4 设置操作间依赖,及优先级,启动操作

在介绍多线程编程的时候我们需要明确一般会在什么场合上使用多线程,我们知道每个进程一定有一个线程–主线程,在这个线程中一般用于更新界面相关的任务,一般任务又可以分成耗时的和非耗时操作,计算密集型的任务和io密集型任务就属于耗时任务,比如读写数据库,读写磁盘文件,访问网络等,这些一般放在子线程中完成,但是一般在任务完成等时候都会将结果呈现在界面上,这时候就需要在主线程中完成。这个大家应该都知道,但是往往很多人会有误区,是不是线程越多越好,答案是否定的,创建的线程过多有如下问题:

  • 从空间角度来看:每个线程都需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程切换需要上下文切换,这就需要耗费一定的时间,线程越多,CPU在调度线程上的开销就越大,同样会降低程序的性能
  • 线程越多,线程关系越复杂,线程竞争,线程管理,以及死锁等其他多线程问题发生的概率就会相应的增加。

因此合理得管理多线程是十分必要的工作。

下面将从:
1.多线程基本概念
2.线程通讯
3.线程同步,线程安全

三个大方面对iOS多线程技术进行一个简要的总结

1.多线程基本概念
1.1 多线程编程的基本概念:

进程: 进程是指系统中正在运行的一个应用程序,进程之间是独立的,有自己专用且受保护的内存空间。
线程: 是操作系统能够进行运算调度的最小单位,是一个CPU执行的一条无分叉的命令序列,进程是由至少一个线程(主线程)构成,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符等。但每个线程都拥有自己的栈,寄存器,本地存储
并行,串行: 是针对线程队列的,表示一次可以执行多少个线程,串行队列每次只能执行一个线程,并行队列可以同时执行多个线程。
同步,异步: 是针对线程行为的,指明线程的执行是否要等到任务返回后再往下执行。
线程安全: 指代码在多线程或者并发任务下能够被安全调用,而不会引起任何问题

1.2 iOS多线程实现方案对比

iOS 多线程方案有如下几种:

  • pthread
    语言: C 语言
    优点:跨平台,可移植
    缺点:需要自己管理线程生命周期,所以用得比较少

  • NSThread
    语言: OC 语言
    优点:是针对pthread的面向对象的封装
    缺点:需要自己管理线程生命周期,使用上还是显得比较麻烦,一般用于查看当前线程状态等不涉及线程周期的场景。

  • GCD
    语言: C 语言
    优点:能够充分发挥多核的特性,自动管理线程生命周期不需要手动管理

  • NSOperation
    语言: OC 语言
    优点:基于GCD 底层的面向对象封装,添加了线程依赖,并发数控制等功能

1.3 NSThread的使用

NSThread的创建:

/* 
param 1:要执行的方法,
param 2:提供selector的对象,通常是self,
param 3:传递给selector的参数
这种方式是默认start的
*/

[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]

/*
param 1:提供selector的对象,通常是self
param 2:要执行的方法
param 3:传递给selector的参数
*/

NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];

/*
param 1:调用的方法
param 2:传给selector方法的参数
*/

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

NSThread常见属性:

//只读属性,线程是否在执行
thread.isExecuting;
//只读属性,线程是否被取消
thread.isCancelled;
//只读属性,线程是否完成
thread.isFinished;
//是否是主线程
thread.isMainThread;
//线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高
thread.threadPriority;
//线程的堆栈大小,线程执行前堆栈大小为512K,线程完成后堆栈大小为0K
thread.stackSize;

NSThread常用方法:

[thread start]; 启动线程
[NSThread exit]; 退出线程
[NSThread isMainThread]; 当前线程是否为主线程
[NSThread isMultiThreaded]; 是否多线程
[NSThread mainThread]; 返回主线程的对象
[NSThread currentThread];(1 表示主线程,其他表示后台线程)
[NSThread sleepUntilDate:[NSDate date]]; (休眠到指定时间)
[NSThread sleepForTimeInterval:4.5]; (休眠指定时长)
1.4 GCD 的使用

使用GCD 需要明确:需要执行哪些操作,要投递到哪种分发队列,怎么执行这些任务串行还是并行。它有两个核心概念“任务”和“队列”,我们只需专注于想要执行的“任务” block,然后添加到适当的“队列”中,剩余的多线程生命周期管理都是GCD来替我们完成。

1.4.1 GCD 的队列类型

GCD 有两大类队列:

  • 串行队列(Serial Dispatch Queue):

串行队列每次只能执行一个任务,但是在应用中可以创建多个串行队列

dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_SERIAL);

iOS 默认创建的主线程就是串行队列,获取串行队列可以通过如下方法获取:

dispatch_get_main_queue()

串行队列的一个很重要的用途就是用于解决数据竞争,因为在串行队列中两个任务不可能并发运行,所以就没有可能会同时访问同一个临界区的风险。所以仅对于这些任务而言,这种运行机制能够保护临界区避免发生竟态条件

  • 并行队列(Concurrent Dispatch Queue):

并行队列和散弹枪一样每次可以同时执行多个任务,但是在系统中对同时执行的任务数是有限制的,这取决于CPU核数以及CPU负载等因素决定。

dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_CONCURRENT);

和串行队列一样iOS系统为创建并行队列增加了全局并行队列:

dispatch_get_global_queue()

全局队列有四个优先级:

DISPATCH_QUEUE_PRIORITY_HIGHT       高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认先级
DISPATCH_QUEUE_PRIORITY_LOW 低先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台优先级

1.4.2 GCD 任务组 (dispatch_group)
  • “对于并行队列,以及多个串行、并行队列混合的情况我们如何知道所有任务都已经执行完了”
  • “如何在某些任务执行完毕后,执行一个操作“

遇到这种情况我们就可以使用GCD 任务组来解决,GCD 任务组 能够在任务组中的任务执行完毕后,执行某个任务。

//创建调度组
dispatch_group_t group = dispatch_group_create();
//将调度组添加到队列,执行 block 任务
dispatch_group_async(group, queue, block);
//阻塞当前线程,等待 group 关联的所有 block 执行完毕或者到达指定时间。如果到达指定时间后,所有任务并没有全部完成,那么 dispatch_group_wait 返回一个非 0 的数,可以根据这个返回值,判断是否等待超时。如果设置为 DISPATCH_TIME_FOREVER ,意思是永远等待,直到所有 block 执行完毕
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//当调度组中的所有任务执行结束后,获得通知,统一做后续操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);

//将任务放到任务组
void dispatch_group_enter(dispatch_group_t group);
//将任务从任务组取出
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_async(group, queue, ^{ 
  // block
});

等价于

dispatch_group_enter(group);
dispatch_async(queue, ^{
  // block
  dispatch_group_leave(group);
});

dispatch_group 例子:

dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});

1.4.3 任务派发函数
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});

dispatch_async(queue, ^{
// 这里放异步执行任务代码
});

dispatch_sync 函数将一个任务添加到一个队列中,会阻塞当前线程,直到该任务执行完毕。dispatch_async 不会等待任务执行完,当前线程会继续往下走,不会阻塞当前线程。

在使用的时候需要特别注意不要往当前队列中使用dispatch_sync抛任务,这样很容易造成死锁。
1.4.4 GCD 停止和恢复
dispatch_suspend(queue) //暂停某个队列  
dispatch_resume(queue) //恢复某个队列
1.4.5 GCD 其他方法
  • GCD 栅栏方法:dispatch_barrier_async

要异步执行多组操作,且前一组操作执行完之后,才能开始执行后一组操作。这种情况就需要用到栅栏来隔离。

dispatch_queue_t queue = dispatch_queue_create("com.dnduuhn.test", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
// 追加任务1
});
dispatch_async(queue, ^{
// 追加任务2
});

dispatch_barrier_async(queue, ^{
// 追加任务 barrier
});

dispatch_async(queue, ^{
// 追加任务3
});
dispatch_async(queue, ^{
// 追加任务4
});
  • GCD 延时执行方法:dispatch_after

延迟一段时间后执行某个操作:

dispatch_after函数传入的时间参数,并不是指在这时间之后开始执行处理,而是在指定时间之后将任务追加到队列中。并且这个时间不是绝对准确时间,但是可以满足对时间不是很严格的延迟要求。

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

});

  • GCD 一次性代码:dispatch_once

使用 dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
  • GCD 快速迭代方法:dispatch_apply

dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(6, queue, ^(size_t index) {
// 迭代任务
});
  • GCD 信号量: Dispatch Semaphore

信号量是持有计数的信号,使用它控制对有限资源的使用和访问。假设有一间房子,它对应一个进程,房子里的两个人就对应两个线程。这个房子(进程)有很多资源,比如花园、客厅、卫生间等,是所有人(线程)共享的。但是有些地方,比卫生间,最多只能有1个人能进去。怎么办呢,在卫生间门口挂1把钥匙。进去的人(线程)拿着钥匙进去(信号量 -1),外面的人(线程)没有钥匙就在门口等待,直到里面的人出来并把钥匙重新放回门口(信号量+1),此时外面等待的人再拿着这个钥匙进去,所有人(线程)就按照这种方式依次访问卫生间这个有限的资源。门口的钥匙数量就称为信号量(Semaphore)。信号量为0时需要等待,信号量不为零时,减去1而且不等待。


dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
dispatch_semaphore_signal:解锁,释放一个信号量,使得信号量值加1
dispatch_semaphore_wait:加锁,信号量减去1,当信号总量为0时就会阻塞所在线程,否则就可以正常执行。

例子:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
//加锁 信号量 -1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//访问临界区数据

//解锁 信号量 +1
dispatch_semaphore_signal(semaphore);
});
}
1.5 NSOperation 的使用

下图中列举了NSOperation 以及NSOperationQueue的一些重要概念:

1.5.1 创建操作

调用Operation的start方法将会在当前线程中执行,将operation添加到Queue不用调用start方法,会自动调度操作在对应线程中运行

NSInvocationOperation:

// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operation1) object:nil];
// 2.调用 start 方法开始执行操作
[invocationOperation start];

NSBlockOperation:

NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。

// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do something
}];

// 2.调用 start 方法开始执行操作
[op start];

通过 addExecutionBlock 添加额外操作,这些操作(包括blockOperationWithBlock中的操作)可以在不同的线程中并发执行。只有当所有相关的操作已经完成执行时,才视为操作完成

// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do something
}];

// 2.添加额外的操作
[op addExecutionBlock:^{
// do additional something
}];

// 3.调用 start 方法开始执行操作
[op start];
}
1.5.2 创建操作队列

获取主队列,凡是添加到主队列中的操作,都会放到主线程中执行

NSOperationQueue *queue = [NSOperationQueue mainQueue];

自定义队列创建方法:添加到这种队列中的操作,就会自动放到子线程中执行

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
1.5.3 设置操作队列属性

设置最大并发数等

1.5.4 将操作添加到操作队列

addOperation

[operationQueue addOperation:op1];
[operationQueue addOperation:op2];
[operationQueue addOperation:op3];

addOperationWithBlock

[operationQueue addOperationWithBlock:^{
// do something
}]
;

1.5.4 设置操作间依赖,及优先级,启动操作
文章目錄
  1. 1. 1.多线程基本概念
    1. 1.1. 1.1 多线程编程的基本概念:
    2. 1.2. 1.2 iOS多线程实现方案对比
    3. 1.3. 1.3 NSThread的使用
    4. 1.4. 1.4 GCD 的使用
    5. 1.5. 1.4.1 GCD 的队列类型
    6. 1.6. 1.4.2 GCD 任务组 (dispatch_group)
    7. 1.7. 1.4.3 任务派发函数
    8. 1.8. 1.4.4 GCD 停止和恢复
    9. 1.9. 1.4.5 GCD 其他方法
    10. 1.10. 1.5 NSOperation 的使用
    11. 1.11. 1.5.1 创建操作
    12. 1.12. 1.5.2 创建操作队列
    13. 1.13. 1.5.3 设置操作队列属性
    14. 1.14. 1.5.4 将操作添加到操作队列
    15. 1.15. 1.5.4 设置操作间依赖,及优先级,启动操作