- 概述及基本概念
1.进程和线程
2.多线程
3.任务
4.队列
5.iOS中的多线程技术
6.GCD和NSOperationQueue的对比
7.使线程同步的方法- NSThread
1.创建和使用
2.线程安全问题
3.线程间通信- GCD
1.串行/并行,同步/异步
2.创建/获取队列
3.线程间通信
4.GCD常用函数- NSOperation
1.常规创建
2.添加操作到队列
3.自定义NSOperation- NSThread+runloop实现常驻线程
概述
1.进程和线程
- 进程
一个运行起来的程序就是一个进程,每个进程之间是独立的,拥有运行所需的全部资源- 线程
程序执行流的最小单元,是进程中的一个实体,一个进程至少有一个线程- 关系
进程的任务是在线程中执行的,进程内,线程共享资源
2.多线程
- 什么是多线程
同一时间内,cup只能处理一条线程,多线程并发其实就是cpu快速的在线程之间调度/切换,造成了多线程的假象- 多线程的优点
1.提高程序的执行效率
2.充分利用设备的多核,提高资源利用率(cpu,内存...)- 多线程的缺点
1.开启线程需要占用一定的内存空间(主线程:1M,子线程512KB, iOS下创建线程大约需要90毫秒的时间),大量开启会占用大量内存空间,降低程序的性能
2.如果线程非常多,cpu在N多线程之间调度,消耗大量cpu资源,会降低线程的执行效率
3.程序设计更加复杂:线程间通信,多线程数据共享- 特点
1.多线程执行顺序不确定,有name属性方便确定bug点
2.优先级不能保证完全优先,只是大概率优先,执行完自动销毁,
3.任务
- 任务
我们要执行的代码(块)- 同步sync
不具备开启线程的能力,将同步任务添加到指定队列中,任务执行完毕之前会阻塞线程- 异步async
具备开启新线程的能力,无需等待继续执行下方的任务,不会阻塞线程,无法确认任务的执行顺序
4.队列
- 队列
存放任务的线性表,FIFO(先进先出)- 串行队列
同一时间内,队列中只能执行一个任务(只开启了一个线程),按照任务添加的顺序,一个任务执行完才能执行下一个任务((仅是队列内串行,可以多个串行队列并行))- 并发队列
同一时间内,允许多个任务并发执行(可以开启多个线程),任务之间不会互相等待,且这些任务的执行顺序和执行过程是不可预测的,只有在异步函数下才有效
5.iOS中的多线程技术
//生命周期,线程任务执行完毕后释放,而不是出了作用域释放
- pThread(c),使用难度大,需要手动管理线程的生命周期(启动、回收...)
- NSThread(oc),使用简单且灵活,手动管理(performSelector开辟的子线程也是NSThread的另一种体现方式)
- GCD(c)充分利用设备的多核,自动管理
- NSOperation(oc)自动管理,封装了GCD,多了一些实用功能,更面向对象
6.GCD和NSOperationQueue的对比
1.GCD执行的是由block构成的任务,执行效率更高
2.GCD只支持FIFO,而NSOperationQueue可以通过设置并发数、优先级,依赖等调整执行顺序
//所以GCD高效,NSOperationQueue多能,根据需要使用
3.NSOperationQueue可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
4.NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
7.使线程同步的方法
0.加锁 / 阻塞任务
1.nsopreation的依赖关系 / 设置最大并发数
2.GCD的 dispath_group / 信号量机制,栅栏
8.自旋锁与互斥锁
- 常见的自旋锁:atomic
A线程在执行任务时,B会不听的在外面徘徊,一旦A执行完毕,B立马执行 - 常见的互斥锁:@synchronized、NSLock
A线程在执行任务时,B会进入休眠状态,A执行完毕后,B会自动唤醒去执行 - 对比
1.自旋锁的执行效率高于互斥锁
2.如果无法在短时间内获取锁,自旋锁一直占用cup,会降低cpu的执行效率
这里提一下原子属性:atomic(自旋锁-默认为setter方法加锁)
一样会消耗大量的资源,建议使用noatomic,因为开发中出现资源抢夺的可能性极低,也可以尽量将加锁,资源抢夺的业务交给服务端处理
- 死锁
死锁原因: 函数未返回阻塞当前任务 + 队列中任务无法并发执行
解决死锁:1.用异步函数 2.新建异步队列不和main一个队列
//pragma mark -- 常见的死锁
//死锁原因:viewDidLoad的任务也是在主队列上的,由于队列的先进先出原则
/*viewDidLoad 来了-- dispatch_sync 来了
viewDidLoad想要执行完走人,viewDidLoad 就得执行完毕,
这时候添加了dispatch_sync,根据FIFO原则,viewDidLoad执行完,dispatch_sync才能走,
但是(串行)viewDidLoad 没执行完,就不会执行dispatch_sync,就造成了死锁
*/
//想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"");
});
NSLog(@"");
}
扩展
不管同步还是异步,在同一个队列中添加任务,就是串行,就会造成阻塞
NSThread
1.创建和使用
//(4个状态:创建,就绪,阻塞,结束)
//实例方法,创建
NSThread * thred1 = [[NSThread alloc]initWithTarget:self selector:@selector(hh) object:nil];
//设置线程的属性要在start之前
thred1.name = @"thred1";
//优先级
thred1.threadPriority = 1;//To be deprecated; use qualityOfService below
[thred1 start];//运行/就绪切换
thred1.qualityOfService = 1; //read-only after the thread is started
[NSThread sleepForTimeInterval:1];//阻塞方式1
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];//阻塞方式2
[NSThread exit];//手动强制结束,
// 获取/判断线程
[NSThread mainThread];
[NSThread isMainThread];
[NSThread currentThread];
//下面2种创建线程的方法简单,但是无法拿到线程对象,进行设置优先级等
//类方法-分离子线程,不需要start
[NSThread detachNewThreadSelector:@selector(hh) toTarget:self withObject:nil];
//隐式创建后台线程(开辟子线程)
[self performSelectorInBackground:@selector(hh) withObject:nil];
/*
//当前线程延迟1秒执行(使用带有参数afterDelay的方法,内部会创建一个NSTimer添加到当前线程中,如果在子线程中,需要开启runloop)
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(showImage:) withObject:nil afterDelay:1.0];
//在指定线程执行,waitUntilDone :是否等待方法内代码执行完毕后再去执行本行代码后面的代码
[self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
//主线程执行
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
*/
2.线程安全问题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//数据安全问题,多线程访问同一资源
self.count = 10;
self.threadA = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
self.threadB = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
self.threadC = [[NSThread alloc]initWithTarget:self selector:@selector(mai) object:nil];
self.threadA.name = @"threadA";
self.threadB.name = @"threadB";
self.threadC.name = @"threadC";
[self.threadA start];
[self.threadB start];
[self.threadC start];
}
- (void)mai
{
while (1) {
if (self.count > 0) {
//添加一个耗时操作,让卖票员手速变慢
for (int i = 0; i < 1000000; i++) {
int a = 1+i;
}
self.count--;
NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
}else{
NSLog(@"mei");
break;
}
}
}
输出结果:(这样目测更清晰)
[2427:109348] threadA卖出了一张票,还剩下9张
[2427:109350] threadC卖出了一张票,还剩下8张
[2427:109349] threadB卖出了一张票,还剩下7张
[2427:109348] threadA卖出了一张票,还剩下6张
[2427:109350] threadC卖出了一张票,还剩下5张
[2427:109349] threadB卖出了一张票,还剩下4张
[2427:109350] threadC卖出了一张票,还剩下3张
[2427:109348] threadA卖出了一张票,还剩下2张
[2427:109349] threadB卖出了一张票,还剩下1张
[2427:109350] threadC卖出了一张票,还剩下0张
[2427:109350] mei
[2427:109348] threadA卖出了一张票,还剩下-1张
[2427:109348] mei
[2427:109349] threadB卖出了一张票,还剩下-2张
[2427:109349] Mei
由上可以看出,由于线程默认是异步的,资源竞争时,会发生我们不想看到的问题
那么接下来解决问题,添加互斥锁
- (void)mai
{
while (1) {
//互斥锁,实现线程同步,但是会小号大量CPU资源
@synchronized (self) {
if (self.count > 0) {
//添加一个耗时操作,让卖票员手速变慢
for (int i = 0; i < 1000000; i++) {
int a = 1+i;
}
self.count--;
NSLog(@"%@卖出了一张票,还剩下%d张",[NSThread currentThread].name,self.count);
}else{
NSLog(@"mei");
break;
}
}
}
}
3.线程间通信
以下载图片为例,贴代码了,不过多解释
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{ //线程间通信
[NSThread detachNewThreadSelector:@selector(downLoadImage) toTarget:self withObject:nil];
}
- (void)downLoadImage
{
NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
//计算下载时间,方法随意,这是其一
NSDate * std = [NSDate date];
NSData *imageData = [NSData dataWithContentsOfURL:url];
NSDate * end = [NSDate date];
NSLog(@"%f",[end timeIntervalSinceDate:std]);//0.404483
UIImage *image = [UIImage imageWithData:imageData];
//一个简单的做法(performSelector方法,可以由任何继承nsobject的对象调用)
[self.imagev performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}
- (void)showImage:(UIImage *)image
{
self.imagev.image = image;
}
GCD
1.串行/并行,同步/异步
- 由此,两两组合,有4中情形(过于基础,有兴趣的自己尝试)
1.异步+并行:多个任务会开启多个线程,队列中的任务的并行的
2.异步+串行:多个任务只会开启1个线程,队列中的任务的串行的
3.同步+并行:多个任务不会开启线程,任务是串行的(在主线程)
4.同步+串行:多个任务不会开启线程,任务是串行的(在主线程)
注意:
1.并不是有多少个任务就开启多少个线程,由系统决定
2.异步+主队列:所有任务都在主队列中执行,不会开启新的线程
3.同步+主队列:死锁
2.创建/获取队列
GCD有3种队列类型
1.mainqueue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列
2.globalqueue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue
3.自定义队列:通过函数dispatch_queue_create创建的队列
- 创建队列
//创建并发和串行队列:2个参数,一个标记字符串, 一个串行还是并发的宏
dispatch_queue_t concurrent_queue = dispatch_queue_create(@"demo_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serial_queue = dispatch_queue_create(@"demo_serial_queue", DISPATCH_QUEUE_SERIAL);
- 获取队列(本身存在队列)
//获取主队列(串行) 及 常用的全局队列(并发)
dispatch_queue_t main_queue = dispatch_get_main_queue();
////2个参数, 一个是优先级, 另一个是 : 保留供将来使用的标志。总是为这个参数指定0。
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
3.线程间通信
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2567670815,24101428&fm=26&gp=0.jpg"];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
self.imagev.image = image;
});
});
}
4.GCD常用函数
- 1.延迟
//5. dispatch_after 延时,内部使用的是dispatch_time_t 管理时间,子线程中不用关心runloop是否开启
dispatch_time_t d = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 3.0));
dispatch_after(d, dispatch_get_main_queue(), ^{
NSLog(@"hhh");
});
相对于 performSelector 和 NSTimer,它的优点是,可以控制在主线程还是子线程执行
- 2.单例
static objectA * ob = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ob = [[objectA alloc]init];
});
reture ob;
- 3.栅栏函数
//栅栏函数:先执行前两个函数,然后执行完栅栏函数才执行后续函数(不能使用全局并发队列)
//dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
dispatch_queue_t concurrent_queue = dispatch_queue_create(@"concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrent_queue, ^{
NSLog(@"0000");
});
dispatch_async(concurrent_queue, ^{
NSLog(@"1111");
});
//栅栏函数(遵循同步/异步规则)
dispatch_barrier_sync(concurrent_queue, ^{
NSLog(@"++++++++");
});
dispatch_async(concurrent_queue, ^{
NSLog(@"2222");
});
- 4.快速迭代
//快速迭代(类似for循环:主线程操作),会开启子线程完成任务,任务的执行是并发的,效率更高
//参数:遍历的次数 队列(并发) 索引
dispatch_apply(100, dispatch_get_global_queue(0, 0), ^(size_t ind) {
//场景比如:大量文件操作
});
- 5.组队列
//dispatch_group_t 监听任务的执行情况
//等待一组操作都完成后执行后续操作(如大图分成几块下载,下载后拼接)
dispatch_queue_t global_queu = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t grou = dispatch_group_create();
dispatch_group_async(grou, global_queu, ^{/*操作1*/});
dispatch_group_async(grou, global_queu, ^{/*操作2*/});
dispatch_group_async(grou, global_queu, ^{/*操作3*/});
//拦截通知:当队列组中的任务都执行完毕时,会执行这个方法
dispatch_group_notify(grou, dispatch_get_main_queue(), ^{
/*后续操作-合并图片0绘图*/
});
/*
dispatch_group_notify(grou, global_queu, ^{
//后续操作-合并图片-绘图
dispatch_async(dispatch_get_main_queue(), ^{
//更新ui
})
});
*/
//阻塞等待组内所有任务执行完毕后,执行之后的代码,效果同上dispatch_group_notify,栅栏函数一样也可以实现
dispatch_group_wait(grou, DISPATCH_TIME_FOREVER);
- 6.信号量:Dispatch Semaphore
用途:(有需要自行研究)
1.保持线程同步,将异步执行任务转换为同步执行任务
2.保证线程安全,为线程加锁
NSOperation
1.常规创建
//NSOperation 是基于GCD的一个抽象(基)类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强,可以加入 操作依赖 控制操作的执行顺序,设置队列最大可并发执行的操作个数,取消操作等,GCD不能中途取消
//NSBlockOperation 执行代码块
//NSInvocationOperation 执行指定的方法
//配置完成后便可以调用start函数在当前线程执行,如果要异步,加入NSOperationQueue中异步执行
//NSInvocationOperation 常规使用
//创建操作,在主线程中执行,类似于直接调用方法
NSInvocationOperation * inv = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(hh) object:nil];
[inv start];
//NSBlockOperation 常规使用
//可以后续添加block块,操作启动后会在不同线程并发执行这些执行快
NSBlockOperation * bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"dd");//任务
}];
//追加任务,如果一个操作中的任务数大于1,那么会开子线程并发执行任务
[bo addExecutionBlock:^{
NSLog(@"aa");//任务
}];
[bo start];
2.添加操作到队列
//获取主队列
NSOperationQueue * que = [NSOperationQueue mainQueue];
//创建非主队列,具备并发+串行,默认是并发队列
NSOperationQueue * quu = [[NSOperationQueue alloc]init];
//可以通过设置最大并发数量(同一时间最多有多少任务可以执行)来设置串行,注意,串行!=只开一条线程,只是任务按序执行
//默认值-1:表示最大值,大于1:并发,等于1:串行,等于0:不执行任务
quu.maxConcurrentOperationCount = 1;
//创建abc 3个操作
NSOperation * a = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"o");
}];
NSOperation * b = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"m");
}];
NSOperation * c = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"g");
}];
/*
//快捷创建操作并添加到队列
[quu addOperationWithBlock:^{
NSLog(@"addOperationWithBlock");
}];
*/
//添加操作依赖 c依赖于a b,c在a b完成后执行
[c addDependency:a];
[c addDependency:b];
//添加操作到队列
[que addOperation:c];
[que addOperation:b];
[que addOperation:a];
3.自定义NSOperation
1.自定义一个继承于NSOperation的子类,实现main方法
//告诉要执行的任务是什么,自动调用
-(void)main
{
NSLog(@"开线程了吗 %@",[NSThread currentThread]);
//[980:25414] 开线程了吗 <NSThread: 0x600003cf7ac0>{number = 7, name = (null)}
//[980:25411] 开线程了吗 <NSThread: 0x600003cdddc0>{number = 5, name = (null)}
}
2.调用
- (void)yshOperation
{
//创建操作/封装性,提高复用性,不需要关注内部实现
YSHOperation * ysho1 = [[YSHOperation alloc]init];
YSHOperation * ysho2 = [[YSHOperation alloc]init];
//创建队列
NSOperationQueue * oq = [[NSOperationQueue alloc]init];
//添加操作
[oq addOperation:ysho1];
[oq addOperation:ysho2];
}
4.常用属性及方法
que.suspended = YES/NO;//暂停/恢复
[que cancelAllOperations];
//值得一提的是
//自定义NSOperation中,main方法内为一个任务,无法对此方法中的操作进行 暂停和取消
//内部有多个耗时任务(for循环大量数据)时,如果想要取消,在耗时操作(for循环)后添加,不建议在for循环内判断,耗费性能
/*
if (self.isCancelled) {//cancelAllOperations内部会判断这个属性,yes则取消
return;
}
*/
NSThread+runloop实现常驻线程
由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程
那么我们可以用GCD实现一个单例来保存NSThread
+ (NSThread *)shareThread
{
static NSThread * shareThread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareThread = [[NSThread alloc]initWithTarget:self selector:@selector(shareThread) object:nil];
[shareThread start];
});
return shareThread;
}
- (void)shareThread
{
NSRunLoop * runl = [NSRunLoop currentRunLoop];
[runl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runl run];
}