多线程
进程
* 进程对应一块内存空间
线程
* 一个进程可以分为多个线程,一个iOS程序运行后,默认会开启一条线程,称为主线程或UI线程 * 网络开发一般使用多线程
时间片
* 人的感知时有延迟的,CPU将时间分为人无法感知的碎片,称为时间片
串行与并行
* 线程是串行的,通过时间片实现伪并行 * 线程调度时无序的,线程启动后的调度顺序是由CPU决定的,程序员无法参与
线程的缺点
* 开启线程需要占用一定的内存空间,在iOS中,主线程栈区占用1MB,子线程占用512K * 线程太多,会占用大量的内存空间,降低程序性能,CPU在调度线程上的开销越大,负荷越大
线程的主要作用
* 显示和刷新UI界面
- 处理UI事件(比如点击事件、滚动事件、拖拽事件等)
线程的使用注意
* 不要将耗时的操作放到主线程中,以避免主线程被阻塞,影响UI流畅度和用户体验
- 不要相信单次运行的结果
线程安全
* 多个线程同时进行操作同一块内存时,读写的数据可能会出现问题
互斥量(mutex)
* 为了避免多个线程同时进行操作同一块内存带来的问题,产生了互斥量的概念,也叫互斥锁、同步锁
-
使用互斥量会阻塞线程
-
加锁的代码范围要尽可能小,只要锁住资源读写部分代码即可
信号量(semaphore)
* 信号量是一个计数器,用来控制访问共享资源的最大并行线程总数,这里的信号量也支持进程(Linux系统中目前不能用于进程)
* 当信号量的计数为1时,效果等价于互斥量
条件变量
线程间通信
XSI IPC
* 共享内存、消息队列和信号量集统称为XSI IPC,遵循相同的POSIX规范
* XSI IPC的同性
* 创建时 都需要提供一个外部key,类型`key_t`(整型)
* key被用来参与 XSI IPC结构(共享内存、消息队列和信号量集)的创建,创建成功后每个IPC结构都有自己唯一的标识(非负整数)ID
* 在创建IPC结构时,都需要提供结构的访问权限
* IPC结构由内核管理,如果不手工删除,重启机器后依然占据空间
* key的创建方式有三种
* 宏 `IPC_PRIVATE`,但基本不用,因为私有别的进程无法获取
* 可以把所有的key定义到一个头文件中,不会发生重复
* 可以用 `ftok()`生成key,`ftok()`使用真实存在的路径 + 项目ID 生成key
* IPC结构的创建/获取函数一般都是
* `xxxget()` 参数中都有key,新建时一般都需要写上 `IPC_CREAT|权限`,返回值就是ID
* IPC结构一般都提供了一个控制函数
* `xxxctl()`,都提供了以下功能
* `IPC_STAT` - 查询
* `IPC_Setter` - 修改
* `IPC_RMID` - 删除
* IPC结构可以用命令查看、删除
* `ipcs` - 查询当前有哪些IPC结构
* `ipcrm` - 删除当前的IPC结构,需要提供ID
* 选项
* `-a` 所有
* `-m` 共享内存
* `-q` 消息队列
* `-s` 信号量集
多线程方法编写建议
* 首先保证一条线程工作正常
* 多线程方法需要保证自己能够独立完成所有任务
* 一般开发不建议使用任何锁
pthread
* Unix/Linux/Windows通用,遵循PROSIX规范,在头文件中均已定义
创建进程
* pthread_create(pthread_t, NULL, , )
* 成功返回0,返回错误编号
线程状态
* join
状态
* join
状态的线程会在函数返回时回收资源
* detach
状态
* detach
状态的线程会在线程结束时回收资源
* 一个线程最好处于join
或detach
之一,否则无法确定其回收资源的时间
取消线程
* pthread_cancel()
* 要取消的线程必须支持取消操作
设置线程状态
* pthread_setcancelstate()
设置取消方式
* pthread_setcanceltype()
结束线程
* return value
* pthread_exit()
等待线程
* pthread_join()
pthread.c
# include \<pthread.h\>
void* task(void* par){
int *pi = (int *)par;
printf("");
if
//pthread_exit();
return 0;
}
int main(){
pthread_t pt;
pthread_create(&pt, NULL, task, );//成功返回0,返回错误编号
pthread_join(pt);
//pthread_cancel(pt);
//pthread_setcancelstate();
//pthread_setcanceltype();
}
互斥量
使用步骤
* 包含头文件
* 声明
* 初始化
* 加锁
* 执行可能造成数据冲突的代码
* 解锁
* 释放互斥量的资源 (销毁)
包含头文件
# include \<pthread.h\>
声明
pthread_mutex_t lock;
初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
pthread_mutex_init(&lock,0);
// 或在声明时赋值:
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock(&lock);
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_unlock(&lock);
释放互斥量的资源 (销毁)
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&lock);
信号量
使用步骤
* 包含头文件
* 定义信号量
* 初始化信号量
* 获取信号量 (计数器减1)
* 访问资源
* 释放信号量 (计数器加1)
* 回收信号量资源 (销毁)
包含头文件
# include \<semaphore.h\>
定义信号量
sem_t sem;
初始化信号量
sem_init(&sem,0,5);
sem_init(信号量指针,0代表用于线程非0用于进程,计数器最大值);
获取信号量 (计数器减1)
sem_wait(&sem);
访问资源
释放信号量 (计数器加1)
sem_post(&sem);
回收信号量资源 (销毁)
sem_destroy(&sem);
条件变量
NSThread
原生方法
* 需要start方法
number属性
* 1为主线程,否则为其他线程
name属性
* 标识线程
threadPriority属性
* 即线程优先级,仅表示CPU对该任务的调度会更加积极
* 范围0.0 \~ 1.0
* 默认值0.5 * 通常开发时,不要处理优先级
* 优先级反转
* 低优先级任务阻塞CPU无法调度高优先级任务
alloc_init
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:id];
thread.name = @"Thread A";
[thread start];
隐式方法
* 放在后台执行
performSelectorInBackGround:withObject:
[obj performSelectorInBackGround:@selector() withObject:nil];
派发任务
* 类方法指定selector方法在后台线程执行,方法会直接调度
* [NSThread detachNewThreadSelector:@selector() toTarget:self withObject:nil]
获取当前线程
* [NSThread currentThread]
线程状态
* 新建(new)
* start => 就绪状态(Runnable)
* CPU调度 => 运行(Running)
* 休眠/同步锁 => 阻塞(Blocked)
* 线程任务结束/异常/强行退出 => 死亡(Dead)
线程休眠
* [NSThread sleepForTimeInterval:1.0f]
* [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0f]]
结束线程
* 任务执行完毕,线程自然结束
* 调用exit
/return
方法,手动强行结束
* 一旦结束线程,后续代码都不会执行
* `NSThread exit]`
互斥量
* 要求是一个能够加锁的NSObject,所有的线程都能够访问到对象
* 在实际开发中,如果只抢夺一个资源,就只需要一把锁,通常使用self
synchronized
@synchronized (self){
// code here...
}
信号量
条件变量
原子锁
* 类似于互斥量,但性能略高
* 在Obj-CC中定义属性时默认都是atomic
* 如果定义了原子属性,就不要重写getter
和setter
方法
* 原子锁本质上是128位自旋锁,能够实现单写多读,保证只有一个线程写入,但允许多个线程读取
* 每一种技术,只要有额外的代码,就需要CPU付出额外的代价,原子属性同样代价很高,性能不好,耗电
* 建议所有属性都声明为nonatomic
线程安全
* UIKit中几乎所有对象都不是线程安全的
* 苹果约定,所有UI控件更新均在主线程中执行,从而保证UI正常显示
* 这也是为什么主线程也称为UI线程
- 有些情况下在其他线程更新UI也是正确的,但一定不要尝试这样的做法
线程间通信
* [obj performSelectorOnMainThread:@selector() : waitUntilDone:BOOL]
GCD
* GCD(Grand Central Dispatch,强大中枢调度器)
* GCD是C语言框架,所有GCD函数均以dispatch开头
* GCD开发时,程序员不需要管线程,而是面对队列开发,将需要的任务添加到不同的队列中
* GCD的优势
-
GCD是苹果公司为多核的并⾏行运算提出的解决⽅方案
-
GCD会⾃自动利⽤用更多的CPU内核(⽐比如双核、四核)
-
GCD会⾃自动管理线程的⽣生命周期(创建线程、调度任务、销毁线程)
-
程序员只需要告诉GCD想要执⾏行什么任务,不需要编写任何线程管理代码
-
MRC的GCD开发中,遇到
create
,copy
,retain
时需要dispatch_release(dispatch_queue_t p)
函数释放 -
ARC的GCD开发中,不需要释放
任务
* 用block来定义
* 任务决定是否开启新线程,同步不开启新线程,异步会开启新线程
同步任务
* 顺序执行任务
* 同步任务只有一个用处,即阻塞后面的所有任务,等待同步任务完成后再并发执行
* dispatch_sync(dispatch_queue_t queque,^{})
异步任务
* 要在别的线程并发执行,会开启新的线程
* 异步是多线程的代名词
* dispatch_async(dispatch_queue_t queque,^{})
队列
* 队列专门用来调度任务,安排不同的任务去不同的线程工作
* 队列决定能够开启线程的数量,串行最多一条,并发最多线程数量由GCD决定
* 队列也可以不开启线程
* 队列在调度任务时,如果正在执行的是同步任务,会等待同步任务执行完成后再调度后续的任务
串行队列
* 顺序的队列,所有任务顺序执行,最多开启一条线程
并发队列
* 能够开启多条线程
主队列
* 专门用来做线程间通讯,后台线程完成工作后,通知主线程更新UI
* 主队列只能获取,不能创建
* 主队列调度任务在主线程工作,不能开启线程
* 队列调度任务是先进先出,主队列不能开启线程,那么只能顺序执行任务
* 主队列中只能使用异步任务,否则会造成死锁
全局队列
* 全局队列在iOS 7和iOS 8中有区别
* 全局队列是方便程序员使用,可以直接使用的整个系统全局共享的队列
* 全局队列只能获取,不能创建
全局队列与手动并发队列
* 全局队列本质上就是并发队列,系统全局的调度队列,可以方便程序员使用
* 创建并发队列有一个好处,在大型商业应用中,通常需要跟踪具体的任务是由哪个队列调度的,如果程序闪退,会产生日志,发送给开发者,便于开发者解决问题,如果能够记录崩溃所在队列的名称,方便排错
* dispatch_get_global_queue(long identifier,unsigned long flags)
* 参数
* `long identifier`
* iOS 8中为标识符,指定队列的服务质量,QOS与XPC框架联合使用,XPC是OS X开发中用于进程间通讯的框架
* `QOS_CLASS_UNSPECIFIED`,即0,表示没有使用QOS
* iOS 7中为优先级
* `DISPATCH_QUEUE_PRIORITY_HIGH`,即2,高优先级
* `DISPATCH_QUEUE_PRIORITY_DEFAULT`,即0,默认
* `DISPATCH_QUEUE_PRIORITY_LOW`,即-2,低优先级
* `DISPATCH_QUEUE_PRIORITY_BACKGROUND`,即INT16_MIN,后台优先级
* 如果使用后台优先级,工作性能会慢的令人发指
* 传入0就可以保证iOS 7与iOS 8全局队列的适配
* `unsigned long flags`,为未来使用保留,应该始终传入0
基本使用
使用步骤
* 定制任务
-
确定想做的事情
-
将任务添加到队列中
-
GCD会⾃自动将队列中的任务取出,放到对应的线程中执⾏行
-
任务的取出遵循队列的FIFO原则,即先进先出,后进后出
-
串行队列+同步任务
* 不会开启新线程,顺序执行所有任务
* 任务顺序执行的原因是同步任务
* 在开发中几乎不用
queue_serial_sync
// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_SERIAL);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_sync(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
串行队列+异步任务
* 开启一条新线程,顺序执行所有任务
* 任务顺序执行的原因是串行队列
queue_serial_async
// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_SERIAL);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_async(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
并发队列+异步任务
* 会开启多条线程,任务不是顺序执行
queue_concurrent_async
// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_async(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
并发队列+同步任务
* 不会开启新线程,顺序执行所有任务
* 任务顺序执行的原因是没有开启新线程
queue_concurrent_sync
// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_sync(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
主队列+异步任务
* 不会开启新线程,顺序执行所有任务
* 任务顺序执行的原因是没有开启新线程
main_queue_async
// 队列
dispatch_queue_t que = dispatch_get_main_queue();
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_async(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
主队列+同步任务
* 不会开启新线程,会造成死锁
main_queue_sync
// 队列
dispatch_queue_t que = dispatch_get_main_queue();
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_sync(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
全局队列+异步任务
- 会开启多条线程,任务并发执行
global_queue_async
// 队列
dispatch_queue_t que = dispatch_get_global_queue(0, 0);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
dispatch_async(que, ^{
// code here...
NSLog(@"%@-%d",[NSThread currentThread],i);
});
}
应用场景
同步任务
例:在线小说网站,需要用户登录,然后再下载小说
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 要保证用户登录最先执行
dispatch_async(que, ^{
dispatch_sync(que, ^{
NSLog(@"用户登录-%@",[NSThread currentThread]);
}
dispatch_async(que, ^{
NSLog(@"下载小说 A -%@",[NSThread currentThread]);
}
dispatch_async(que, ^{
NSLog(@"下载小说 B -%@",[NSThread currentThread]);
}
}
队列的选择取舍
* 串行队列最多开启一条线程,任务顺序执行,效率低,执行慢,但省电
* 对执行顺序要求高,对并发要求不高,执行性能要求不高,兼顾电量,可以选择串行队列
* 并发队列可以开启多条线程,任务并发执行,效率高,执行快,但费电
* 对执行效率要求高,对执行顺序要求不高,可以选择并发队列
常用GCD组合
最常见的GCD代码
// GCD是用来处理耗时的任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 耗时的任务
// code here...
// 与主线程通讯,更新UI
dispatch_async(dispatch_get_main_queue(), ^{
// 更新UI
// code here...
});
});
其他GCD用法
延时处理
* dispatch_after(dispatch_time_t when,queue , ^{})
* when
延时
* 从`DISPATCH_TIME_NOW`起经过多少纳秒后在`queue`队列上执行`block`
* dispatch_time(DISPATCH_TIME_NOW,延迟时间_纳秒)
* queue
队列
* block
异步任务
dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",[NSThread currentThread]);
});
// 全局队列会延时多少时间后新建线程执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
NSLog(@"%@",[NSThread currentThread]);
});
// 串行队列会延时多少时间后新建线程执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_queue_create("meniny", NULL), ^{
NSLog(@"%@",[NSThread currentThread]);
});
成组工作
dispatch_group_t gup = dispatch_group_create();
dispatch_queue_t que = dispatch_get_global_queue(0, 0);
// 派发任务
dispatch_group_async(gup, que, ^{
NSLog(@"任务一%@",[NSThread currentThread]);
});
dispatch_group_async(gup, que, ^{
NSLog(@"任务二%@",[NSThread currentThread]);
});
dispatch_group_async(gup, que, ^{
NSLog(@"任务三%@",[NSThread currentThread]);
});
dispatch_group_notify(gup, dispatch_get_main_queue(), ^{
NSLog(@"任务全部完成%@",[NSThread currentThread]);
});
单次执行
* 有些代码只希望执行一次,应用最广泛的是单例设计模式
* dispatch_once()
是线程安全的,在多线程执行的时候仍可以保证只执行一次,它同样使用了锁,而性能比互斥锁高很多,这是苹果推荐的技术,但目前已经达到了滥用的程度
dispatch_once
static dispatch_once_t onceToken;
NSLog(@"%ld",onceToken);
dispatch_once(&onceToken, ^{
// 只执行一次的代码
// code here...
});
NSOperation
-
NSOperation是抽象类,并不具备封装操作的能⼒力,必须使⽤用它的⼦子类
* 使⽤用NSOperation子类的方式有3种
-
NSInvocationOperation
-
NSBlockOperation
-
自定义⼦子类继承NSOperation,实现内部相应的⽅方法
-
-
配合NSOperation和NSOperationQueue也可以实现多线程
-
NSOperation & GCD属于并发开发
-
程序猿只需要面对队列,将操作添加到队列上即可,不用关心线程,也不用关心线程状态
-
NSOperation & GCD一般都不需要我们添加自动释放池
-
NSOperation是对GCD的封装
-
历史
-
NSOperation是iOS 2出现,GCD在iOS 4出现
-
苹果对NSOperation基于GCD面向对象封装
-
NSOperation和NSOperationQueue实现多线程的具体步骤
* 先将需要执⾏行的操作封装到⼀个NSOperation对象中
-
然后将NSOperation对象添加到NSOperationQueue中
-
系统会⾃自动将NSOperationQueue中的NSOperation取出来
NSOperationQueue
* 实例化的NSOperationQueue本质上就是GCD的全局队列
* 添加到队列中的操作都是异步执行的
NSOperationQueue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
NSOperationQueue mainQueue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// upd UI...
}];
- 通常开发中,会使用一个全局队列来管理
NSOperationQueue
@property (nonatomic, strong) NSOperationQueue *opQueue;
- (NSOperationQueue *)opQueue {
if (!_opQueue) {
_opQueue = [[NSOperationQueue alloc] init];
}
return _opQueue;
}
并发线程数
* 在iOS 7中,使用NSOperation或GCD一般只能开启10条左右的线程
* 从iOS 8开始,如果任务很多,线程数量同样很大
* 开启线程是有消耗的,实际开发中有必要控制同时并发的线程数量,这个控制在GCD中较难实现
* maxConcurrentOperationCount:(NSInteger)
最大并发线程数,设定最大并发线程数
* 在GCD底层为了提高程序并发性能,会维护一个线程池,供所有应用程序的多线程使用,每个线程上的任务执行后,会被GCD线程池回收,而不销毁
* 在队列调度任务时,会出现任务执行完毕,但底层线程池还没有完全回收,如果在调度其他任务,系统会从线程池中取出一个空线程供程序使用
何时需要控制最大并发线程数?
* 下载网络资源,判断用户联网状态
* WI-FI:不花钱,能够随时充电,可以让线程数5-6,并发性能好,速度快
* 3G:花钱,不容易充电,可以让线程数少一些,2-3,速度慢
队列暂停与继续调度任务
* op.suspended = YES
挂起,设定YES
或NO
* operationCount
操作数,返回当前操作数
* 如果队列已经调度了任务,挂起操作不会对任务造成影响,任务仍然会继续执行
* 如果任务没有执行完毕,会包含在operationCount中
* 任务执行完成后,队列中的任务技术才会变化
应用场景
* TableView滚动时需要暂停队列的下载,而停止滚动,需要让队列继续下载,这样可以避免很多无谓的网络请求
依赖
* 在GCD中可以通过同步任务要求任务执行的顺序,而NSOperation中只有异步任务
* [op2 addDependency:op1]
指定任务之间的依赖关系,op2需要等op1执行完毕后才可以执行
* 多个任务之间,可以跨队列依赖 * 注意,线程依赖关系不能循环,否则会造成死锁
应用场景
* 网络下载小说的压缩包
* 解压缩保存到磁盘
* 通知用户可以阅读
NSInvocationOperation
* [op start]
* 调用start方法后会自动执行target的selector方法
* 不会创建新线程,默认在当前线程同步执行操作
* 只有添加NSOperation到NSOperationQueue才会异步执行
NSInvocationOperation & start
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector() object:@"hello"];
[op start];
NSInvocationOperation & NSOperationQueue
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector() object:@"hello"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
NSBlockOperation
* - (void)blockOperationWithBlock:(void (^)(void))block
初始化并添加操作
* - (void)addExecutionBlock:(void (^)(void))block
添加更多操作
NSBlockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// code here...
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
NSBlockOperation
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
// code here...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// udp UI
}];
}];
自定义子类继承NSOperation类,实现相应方法
GCD对比NSOperation
CGD
* GCD时C语言的
* 两个队列,两种任务,需要排列组合
* 一次性、分组、延迟等功能时NSOperation不具备的
NSOperation
* NSOperation是Obj-C的
* NSOperation是对GCD的封装
* 新创建的队列就是并发队列,添加到队列中的任务就是异步执行
* 可以设置最大并发数、暂停与继续使用起来比GCD方便
* 可以指定队列之间的依赖关系,代码结构上直观
提示
* 国内大部分公司在使用GCD,很少使用NSOperation
* 苹果强烈建议使用NSOperation,从iOS 8开始,GCD底层调度的线程数量从原有的10以内增加到70以上(测试结果)