iOS开发之多线程
进程
1.进程是指在系统中正在运行
的一个应用程序
2.每个进程之间是相互独立
的,每个进程均运行在其专用且受保护的内存空间中
线程
1.一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程)
2.一个进程(程序)的所有任务都在线程中执行
线程的串行
1.一个线程中任务的执行是串行的
2.如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务
3.也就是说,在同一时间内,一个线程只能执行一个任务
4.因此,也可以认为线程是进程中的一条执行路径
进程和线程的比较
1.线程是CPU调用(执行任务)的最小单位
2.进程是CPU分配资源和调度的单位
3.一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程
4.同一个进程内的线程共享进程的资源
多线程
1.一个进程中可以开启多条线程,每个线程可以并发(同时)执行不同的任务
2.多线程可以提高任务的执行效率
多线程的原理
1.同一时间,CPU只能处理一条线程,只有一条线程在执行(单核)
2.多线程并发(同时)执行,其实是CPU快速的在多条线程之间调度(切换)
3.如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
4.如果线程非常多,会导致CPU在很多的线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)
多线程的优缺点
- 优点:
1.能适当提高程序的执行效率
2.能适当提高资源利用率(CPU,内存利用率) - 缺点:
1.创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1kb),栈空间(子线程512kb,主线程1mb,也可以使用-setStackSize:设置,但必须是4k的倍数,而且最小是16k),创建线程大约需要90毫秒的创建时间
2.如果开启大量的线程,会降低程序的性能
3.线程越多,CPU在调度线程上的开销就越大
4.程序设计更加复杂,比如线程之间的通信,多线程的数据共享
多线程在iOS开发中的应用
-
主线程:
一个iOS程序运行后,默认会开启一条线程,称为主线程或UI线程 -
主线程的主要作用:
1.显示/刷新UI界面
2.处理UI事件(比如点击事件,滚动事件,拖拽事件等) -
主线程的使用注意:
1.不要将比较耗时的操作放到主线程中
2.耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种卡的坏体验 -
耗时操作的执行
将耗时操作放在子线程(后台线程,非主线程) -
获得主线程
NSThread *main=[NSThread mainThread];
-
获得当前线程
NSThread *current=[NSThread currentThread];
-
判断是否是主线程
1.number==1?
2.类方法
BOOL isMain= [NSThread isMainThread];
3.对象方法
BOOL ifMain=[current isMainThread];
- iOS中多线程的实现方案
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API; 适用于unix/linux/windows等系统; 跨平台/可移植; 使用难度大 |
C | 程序员管理 | 几乎不用 |
NSThread | 使用更加面向对象; 简单易用,可直接操作线程对象 |
OC | 程序员管理 | 偶尔使用 |
GCD | 旨在替代NSThread 等线程技术; 充分利用设备的多核 |
C | 自动管理 | 经常使用 |
NSOperation | 基于GCD(底层是GCD); 比GCD 多了一些更简单实用的功能; 使用更加面向对象 |
OC | 自动管理 | 经常使用 |
pthread的简单使用
1.#import <pthread.h>
2. //创建线程对象
pthread_t thread;
//创建线程
/*
第一个参数:线程对象 传递地址
第二个参数:线程的属性 可设为null
第三个参数:指向函数的指针
第四个参数:函数需要接受的参数
*/
pthread_create(&thread, NULL, test, NULL);
void *test(void * param){
return NULL;
}
NSThread的基本使用
- 方法一
/*
第一个参数:目标对象
第二个参数:方法选择器 调用的方法
第三个参数:前面调用方法需要传递的参数 可以为nil
*/
NSThread *subthread=[[NSThread alloc]initWithTarget:self selector:@selector(run:) object:@"123"];
//设置线程的名字
subthread.name=@"subthread";
//优先级 0.0(最低)--0.5(默认)--1.0( 最高)
subthread.threadPriority=1;
//需要手动启动线程
[subthread start];
-(void)run:(NSString *)param{
}
- 方法二
//分离子线程,自动启动线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"分离子线程"];
- 方法三
//开启一条后台线程
[self performSelectorInBackground:@selector(run:) withObject:@"开启一条后台线程"];
-
后两种方法的优缺点
1.优点:
简单快捷
2.缺点:
没有办法拿到线程对象,无法设置优先级或其他属性 -
NSThread的生命周期
当线程中的任务执行完毕后才被释放 -
控制线程的状态
1.创建线程
init
新建, 此时在内存中存在,但是不在可调度线程池中
2.启动线程
-(void)start;
进入就绪状态,进入可调度线程池,当CPU调度时,进入运行状态,当线程任务执行完毕,自动进入死亡状态
3.阻塞/暂停线程
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
进入阻塞状态,推出可调度线程池,当时间到时,再次进入就绪状态,进入可调度池
4.强制停止线程
+(void)exit;
进入死亡状态,一旦线程死亡了,就不能再次开启任务 -
线程安全
多线程的安全隐患
1.资源共享
一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象,同一个变量,同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题 -
安全隐患解决--互斥锁
互斥锁使用格式
@synchronized(锁对象,通常为self){
//需要锁定的代码
}
锁:必须是全局唯一的一把锁,
锁定一份代码只用一把锁,用多把锁是无效的
1.注意加锁的位置
2.注意加锁的前提条件,多线程共享同一块资源
3.注意加锁是需要付出代价的,需要耗费性能
4.加锁的结果:线程同步
线程同步:多条线程在同一条线上执行(按顺序的执行任务)
-
互斥锁的优缺点
优点:
能有效防止因多线程抢夺资源造成的数据安全问题
缺点:
需要消耗大量的CPU资源 -
互斥锁的使用前提:
多条线程抢夺同一块资源 -
原子和非原子属性
oc在定义属性时有nonatomic和atomic两种选择
atomic:原子属性,为setter方法加锁(默认就是atomic)
线程安全,但需要消耗大量的资源
nonatomic:非原子属性,不会为setter方法加锁
非线性安全,适合内存小的移动设备 -
iOS开发的建议
1.所有属性都声明为nonatomic
2.尽量避免多线程抢夺同一资源
3.尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力 -
线程间通信
在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信 -
线程间通信的体现
1.一个线程传递数据给另外一个线程
2.在一个线程中执行完特定任务后,转到另一个线程继续执行任务
线程间通信常用方法
-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
GCD
纯c语言,提供了非常多强大的函数
-
gcd的优势
1.gcd是苹果公司为多核的并行运算提出的解决方案
2.gcd会自动利用更多的cpu内核(比如双核,四核)
3.gcd会自动管理线程的生命周期(创建线程,调度任务,销毁线程)
4.程序员只需要告诉gcd想要执行什么任务,不需要编写任何线程管理代码 -
任务和队列
任务:执行什么操作
队列:用来存放任务 -
gcd的使用就两个步骤
1.定制任务,确定想做的事情
2.将任务添加到队列中,gcd会自动将队列中的任务取出,放到对应的线程中执行 -
任务的取出遵循队列的FIFO原则:先进先出
-
执行任务
同步:只能在当前线程中执行任务,不具备开启新线程的能力,需要立刻执行,执行完毕再执行下面的
dispatch_sync(dispatch_queue_t queue, ^(void)block);
异步:可以在新的线程中执行任务,具备开启新线程的能力,即使没有执行完毕,后面的也可以执行
dispatch_async(dispatch_queue_t _Nonnull queue, ^(void)block);
-
队列
并发队列:可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步函数下才有效
串行队列:让任务一个接着一个地执行(一个任务执行完毕后,在执行下一个任务) -
同步和异步主要影响:能不能开启新的线程
-
并发和串行主要影响:任务的执行方式
-
gcd的基本使用
//1.创建队列
/*
第一个参数:c语言的字符串,就是一个标识符,用来区分队列,没有实际意义
第二个参数:队列类型
DISPATCH_QUEUE_CONCURRENT 并行队列
DISPATCH_QUEUE_SERIAL 串行队列
*/
dispatch_queue_t queue= dispatch_queue_create("123", DISPATCH_QUEUE_CONCURRENT);//方法一
/*
第一个参数:队列优先级
第二个参数:暂时无用,写0即可
*/
dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//方法二,获得全局并发队列
//2.封装任务,添加任务到队列中
/*
第一个参数:队列
第二个参数:要执行的任务
*/
dispatch_async(queue, ^{
});
并发队列 | 串行队列 | 主队列 | |
---|---|---|---|
异步函数 | 会开启多条线程,队列中的任务是并发执行 | 会开启线程,开一条线程,队列中的任务是串行执行 | 不会开启线程,所有任务都在主线程中执行,队列中的任务是串行执行 |
同步函数 | 不会开启线程,队列中的任务是串行执行 | 不会开启线程,队列中的任务是串行执行 | 在主线程中会产生死锁,在子线程中没有影响 |
- 并发队列
两种创建方法:
1.自己创建
dispatch_queue_t queue= dispatch_queue_create("123", DISPATCH_QUEUE_CONCURRENT);
2.获得全局并发队列
dispatch_queue_t queue1=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- 串行队列
两种创建方法
1.自己创建
dispatch_queue_t queue= dispatch_queue_create("123", DISPATCH_QUEUE_SERIAL);
2.获得主队列(和主线程相关的队列)
dispatch_queue_t queue1=dispatch_get_main_queue();
-
主队列是gcd自带的一种特殊的串行队列
-
放在主队列中的任务,都会放到主线程中执行
-
主队列特点
如果主队列发现当前主线程有任务在执行,那么主队列会暂停调用队列中的任务,直到主线程空闲位置 -
gcd实现线程间通信
gcd同步/异步函数嵌套使用同步/异步函数 -
gcd的常用函数
- 延迟执行
/*
第一个参数:DISPATCH_TIME_NOW 从现在开始计算时间
第二个参数:延迟的时间 gcd时间单位:纳秒
第三个参数:队列
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
- 一次性代码,不能放在懒加载中,主要用在单例中
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
- gcd栅栏函数
3.1 作用:控制多线程中并发任务的执行顺序,比如放置在任务1,任务2之后,任务3之前,则结果是等任务1,2都执行完毕后,再执行任务3
3.2 注意:栅栏函数不能使用全局并发队列
/*
第一个参数:队列
第二个参数:操作
*/
dispatch_barrier_async(queue, ^{
//操作
});
- gcd快速迭代(遍历)
for循环是同步的,gcd是开子线程和主线程一起完成遍历任务,任务的执行是并发的
/*
第一个参数:遍历的次数
第二个参数:队列(只能传并发队列,串行队列无效果,主队列会死锁)
第三个参数:index 索引
*/
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"%zd",index);
});
- gcd队列组
gcd队列组的使用
dispatch_queue_t queue2=dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group=dispatch_group_create();
/*
作用:
第一个:封装任务
第二个:把任务添加到队列中
第三个:会监听任务的执行情况,通知group
*/
dispatch_group_async(group, queue2, ^{
});
dispatch_group_async(group, queue2, ^{
});
dispatch_group_async(group, queue2, ^{
});
//拦截通知,当队列组中所有的任务都执行完毕的时候进入到下面的方法
//内部本身是异步的
dispatch_group_notify(group, queue2, ^{
});
老式写法
dispatch_queue_t queue3=dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group1=dispatch_group_create();
//在该方法后面的异步任务会被纳入到队列组的监听范围,进入群组
//enter和leave必须配套出现
dispatch_group_enter(group1);
dispatch_sync(queue3, ^{
//离开群组
dispatch_group_leave(group1);
});
dispatch_group_enter(group1);
dispatch_sync(queue3, ^{
//离开群组
dispatch_group_leave(group1);
});
//等待 等价于dispatch_group_notify,本身是阻塞的,即我不执行,下面的也不执行
//DISPATCH_TIME_FOREVER 表示死等,直到队列组中所有的任务都执行完毕之后才执行,
dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
NSOperation
-
作用:
配合使用NSOperation和NSOperationQueue也能实现多线程编程 -
具体步骤:
1.先将需要执行的操作封装到一个NSOperation对象中
2.将NSOperation对象添加到NSOperationQueue中
3.系统会自动将NSOperationQueue中的NSOperation取出来
4.将取出的NSOperation封装的操作放到一条新线程中执行 -
NSOperation的子类
NSOperation是个抽象类,并不具备封装操作的能力,必须使用他的子类 -
使用NSOperation子类的方式有3中
1.NSInvocationOperation
//1.创建操作,封装任务
/*
第一个参数:目标对象
第二个:调用的方法
第三个:调用的方法的参数
*/
NSInvocationOperation *op1=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
2.NSBlockOperation
//1.创建操作,封装任务
NSBlockOperation *op2=[NSBlockOperation blockOperationWithBlock:^{
//任务
}];
//追加任务
//注意:如果一个操作中的任务数量大于1,那么会开子线程并发执行任务
//注意:不一定是子线程,有可能是主线程
[op2 addExecutionBlock:^{
//任务
}];
3.自定义子类继承NSOperation,实现内部的main方法
//告知要执行的任务是什么
//有利于代码隐蔽,有益于代码复用
-(void)main{
//任务
}
创建好操作后
//2.启动/执行操作
//注意:如果直接start,那么不会创建新线程,就和一般的[self 方法名];效果一样,无法实现多线程编程
[op1 start];
[op2 start];
//2.创建队列,这样才可能实现多线程编程
/*
主队列:[NSOperationQueue mainQueue]和gcd中的主队列一样,是串行队列,在主线程中执行
非主队列:[[NSOperationQueue alloc]init]非常特殊,因为同时具备并发和串行的功能
默认情况下,非主队列是一个并发队列
*/
NSOperationQueue *queue=[[NSOperationQueue alloc]init];
//3. 添加操作到队列中,内部已经调用了start方法
[queue addOperation:op2];
//简便方法 首先创建了NSBlockOperation操作,然后把操作添加到队列里
[queue addOperationWithBlock:^{
}];
NSOperation的其他用法
NSOperationQueue *queue1=[[NSOperationQueue alloc]init];
//设置最大并发数:同一时间最多有多少个操作/任务可以执行
//设置为1则为串行队列,但是不等于只开一条线程
//不能设置为0,因为会不执行任务
//大于1就是并发队列
//设置为-1则代表最大值,不受限制
queue.maxConcurrentOperationCount=5;
//暂停 可以恢复 不能暂停当前正在执行的任务,需要等当前任务执行完毕再暂停
//队列中的任务也是有状态的 已经执行完毕/正在执行/排队等待执行
[queue setSuspended:YES];
//继续
[queue setSuspended:NO];
//取消 不可以恢复 不能暂停当前正在执行的任务,需要等当前任务执行完毕再暂停
//该方法内部调用了所有操作的cancel方法
//自定义NSOperation
gh *op3=[[gh alloc]init];
//需要先在自定义的gh中的main方法中设置
if (op3.isCancelled==YES) {
return;
}
[queue cancelAllOperations];
NSOperation操作依赖和监听
//添加操作依赖
//注意:不能循环依赖,不会崩溃,但是谁都不会执行 可以跨队列依赖
[op1 addDependency:op2];
[op2 addDependency:op3];
//操作监听
op3.completionBlock = ^{
//操作
};
NSOperation实现线程间通信
NSOperationQueue *queue3=[[NSOperationQueue alloc]init];
NSBlockOperation *down=[NSBlockOperation blockOperationWithBlock:^{
//操作
//在主线程中更新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
//在主线程操作UI更新
}];
}];
[queue3 addOperation:down];
以上差不多就是iOS中多线程的知识点了 ,如果哪里有错误,还希望告知一下