很多朋友都说iOS开发中,最难理解和学习的就是多线程,很多的原理实现都是通过log看到,也比较抽象,本人也是在多线程方面投入过很多脑细胞。。无论这方面的知识掌握和应用起来是否轻松,牢固的基本功、正确的认识理解、再加上充分的实战经验,都能助你将其搞定。这里先介绍一些多线程的知识以及应用,作为讨论,大家共同学习。
一、多线程基本概念
1、线程与进程
(1)进程:操作系统的每一个应用程序就是一个进程
(2)线程:进程的基本执行单元,一个进程的所有任务都在线程中执行
2、主线程
(1)定义:一个程序运行后,默认会开启1个线程,称为“主线程”或“UI线程”。其他为“子线程”。
(2)作用及注意:线程一般用来 刷新UI界面 ,处理UI事件(比如:点击、滚动、拖拽等事件),避免将耗时的操作放到主线程,以免造成主线程卡顿。
3、多线程原理:
(1)是CPU快速的在多个线程之间的切换(自身的随机调度算法)。
(2)同步/异步:
- 同步:指的就是在当前线程(不一定是主线程)中,从上而下依次执行任务(代码块的阅读顺序),这个就叫做同步。
- 异步:指不在当前线程中执行了,开辟新的线程执行, 注意:即使在别的线程中执行,也是从上而下依次执行的。
4、iOS多线程实现方案
5、线程的占用空间:
(1)子线程:512KB。
(2)主线程:512KB。这里官方文档给出的是1M,实际测试为512,可以打印线程的stackSize属性验证。
6、线程的状态和生命周期:
(1)控制线程的状态(以NSThread管理线程为例)
a、启动线程:- (void)start;
线程进入就绪状态,当线程执行完毕后自动进入死亡状态。
b、暂停(阻塞)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
线程进入阻塞状态
c、停止线程
+ (void)exit;
线程进入死亡状态
(2)状态图
7、线程的属性(以NSThread管理线程为例,一下是NSTread类中的方法或属性)
(1)stackSize:占内存大小
(2)name:名字
(3)threadPriority:优先级(不推荐使用)
(4) qualityOfService:服务质量
二、多线程深入理解
1、线程间资源共享/抢夺
(1)定义:一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,当多个线程访问同一块资源时,各个线程提取和修改数据不同步,很容易引发数据错乱和数据安全问题。
(2)互斥锁(线程同步) :解决上面的问题
- 代码:@synchronized(锁对象) { 需要锁定的代码 }
- 每一个对象(NSObject)内部都有一个锁(变量),当有线程要进入synchronized到代码块中会先检查对象的锁是打开还是关闭状态,默认锁是打开状态(1),如果是线程执行到代码块内部 会先上锁(0)。如果锁被关闭,再有线程要执行代码块就先等待,直到锁打开才可以进入。
- 互斥锁的实现流程
线程执行到synchronized
i. 检查锁状态 如果是开锁状态转到ii ,如果上锁转到v
ii. 上锁(0)
iii. 执行代码块
iv. 执行完毕 开锁(1)
v. 线程等待(就绪状态)
- 注意:必须使用全局对象来提供“锁”,否则同样锁不住。
2、原子属性
(1)属性中的修饰符
- nonatomic :非原子属性
- atomic : 原子属性,针对多线程设计的,是默认值。保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以取值。
- 自旋锁:atomic 本身就有一把锁(自旋锁),保证“单写多读”:单个线程写入,多个线程可以读取。如果发现有其它线程正在锁定代码,线程会用死循环的方式,一直等待锁定的代码执行完成 。自旋锁更适合执行不耗时的代码。
- iOS开发的建议:
- 所有属性都声明为nonatomic,移动设备内存小,atomic虽然相对线程安全,但是消耗资源较多。
- 尽量避免多线程抢夺同一块资源(多个线程访问和修改同一数据)。
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。
3、线程安全
(1)多个线程同时操作一个全局变量是不安全的,使用自旋锁并不是绝对的安全(因为单写多读)。
(2)线程安全:在多个线程进行读写操作时,仍然能够保证数据的正确 。使用互斥锁可以实现,但是消耗性能。
(3)关于主线程(UI线程):几乎所有UIKit??提供的类都是线程不安全的,所有更新UI的操作都在主线程上执行。
4、NSRunLoop
(1)功能作用:运行循环,又叫消息循环或事件循环。
- 检测、接收 “输入事件” 并执行。
- 保证程序不退出(主线程)。
- 如果没有事件发生,会让程序进入休眠状态。
(2)特点:
- NSRunLoop不能单独存在,必须存在于线程中,不论主线程中还是子线程中都有一个消息循环。
- 主线程的RunLoop是默认开启的,子线程中的RunLoop默认不开启(使用run方法开启)。
- 只要线程一启动,内部就会有一个默认的主RunLoop,而每一个App,只要一启动,就会自动有一个主线程。
(3)(两大核心之一)输入事件:
- 输入源:比如 键盘输入,滚动scrollView,performSelector方法
- 定时源:NSTimer 定时器
(4)(两大核心之二)运行模式(消息循环模式):
- 线程的消息循环运行在某一种消息循环模式上。
- 输入事件必须设置消息循环的运行模式,并且如果想让输入事件可以在消息循环上执行,输入事件的消息循环运行模式必须和当前消息循环的运行模式一致
- 两种常用的运行模式:NSDefaultRunLoopMode、NSRunLoopCommonModes。
- 实例:输入事件是定时源
//定时源 (计时器) NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(demo) userInfo:nil repeats:YES]; /* 参数1:输入源 参数2:输入源的模式,要和当前消息循环的模式对应,才可以让消息循环执行输入源 NSDefaultRunLoopMode默认模式 NSRunLoopCommonModes包含了很多种模式 */ [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
5、自动释放池
(1)主线程自动释放池的创建和销毁:
- 每一次主线程的消息循环开始的时候会先创建自动释放池。
- 消息循环结束前,会释放自动释放池。
- 自动释放池被销毁或耗尽时会向池中所有对象发送 release 消息,释放所有 autorelease 的对象(引用计数-1)。
- 自动释放池随着消息循环的开始和结束不断的重建和销毁。
(2)子线程的自动释放池:
- 在子线程开启时手动创建释放池。因为主线程可以自动生成释放池,而子线程不可以。为了保证消息循环结束(线程结束)时,所有的对象可以正常入池和释放,必须手动添加。
- 其他情况和主线程相同。
(3)什么时候使用自动释放池:(官方文档建议)
- 开启子线程时。
- 在一个循环中,生成了大量的临时变量,需要手动在循环内部加入释放池(否则内存会爆。。)。
- 例如:
for (int i = 0; i < largeNumber; ++i) { @autoreleasepool { NSString *str = @"Hello World"; str = [str stringByAppendingFormat:@" - %d", i]; str = [str uppercaseString]; } }
(4)示意图
三、线程管理————pthread
1、一套通用的多线程API,纯C语言,操作难度大,在iOS开发中基本不使用。
2、基本使用方式
#import <pthread.h> //线程编号的地址,本质是结构体类型 pthread_t pthread; //方法的返回值:0 成功, 其它失败 int result = pthread_create(&pthread, NULL, demo, NULL); /* pthread_create函数的参数介绍 第一个参数: 线程编号的地址; 第二个参数: 线程的属性; 第三个参数: 线程要执行的方法,其中void *(*)(void *)具体代表含义: //函数的返回值类型: void *,类似于oc中的id; //函数的名称:函数指针; //函数的参数:void *; 第四个参数:线程要执行的方法的参数。 */
四、线程管理————NSThread
1、创建新线程的三种方式,例如:
//方式一:NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:nil]; [thread start];//方式二: [NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:nil]; //方式三:准确的说此方法是NSObject的 [self performSelectorInBackground:@selector(demo:) withObject:nil];
2、NSThread在调试中的使用
- 获得线程的属性:name,stackSize,threadPriority(默认0.5)
- 管理线程的类方法:start、exit、sleep
- 获得当前线程和主线程: [NSThread currentThread] 、[NSThread mainThread];
五、线程管理————GCD
在之前文章中,已经针对GCD的基本使用做了详细介绍,不在这里复述了。可参阅:http://www.cnblogs.com/cleven/p/5249246.html
本文只对GCD的其他操作进行一些补充。
1、延迟操作
实例:
//延时操作 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ }); /* dispatch_after的参数 参数1 dispatch_time_t when 多少秒之后执行 参数2 dispatch_queue_t queue 任务添加到那个队列 参数3 dispatch_block_t block 要执行的任务 */
2、一次性执行
(1)定义:程序运行中只执行一次。一次性执行是线程安全的,可以使用一次性执行创建单例对象,效率比互斥锁高。
(2)实现:可以用来创建单例对象。
//原理:当onceToken为0时执行方法,然后将全局变量oneceToken更改为-1,以后就无法再执行。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //要执行一次的代码; });
3、调度组
(1)定义:有时候需要在多个异步任务都执行完成之后继续做某些事情,比如下载歌曲,等所有的歌曲都下载完毕之后转到主线程提示用户,这样需要一个顺序的统一调度。
(2)实现:
//1 全局队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //2 调度组 dispatch_group_t group = dispatch_group_create(); //3 添加任务 //把任务添加到队列,等任务执行完成之后通知调度组,任务是异步执行 dispatch_group_async(group, queue, ^{ NSLog(@"歌曲1下载完毕 %@",[NSThread currentThread]); }); dispatch_group_async(group, queue, ^{ NSLog(@"歌曲2下载完毕 %@",[NSThread currentThread]); }); dispatch_group_async(group, queue, ^{ NSLog(@"歌曲3下载完毕 %@",[NSThread currentThread]); }); //4 所有任务都执行完成后,获得通知 (异步执行) //等调度组中队列的任务完成后,把block添加到指定的队列 dispatch_group_notify(group, queue, ^{ NSLog(@"所有歌曲都已经下载完毕! %@",[NSThread currentThread]); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //在主线程,更新UI控件,提示用户 NSLog(@"播放器更新完毕! %@",[NSThread currentThread]); }); NSLog(@"over");
(3)原理:
//1 全局队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //2 调度组 dispatch_group_t group = dispatch_group_create(); //ARC中不用写 // dispatch_retain(group); //3 进入调度组,执行此函数后,再添加的异步执行的block都会被group监听 dispatch_group_enter(group); //4 添加任务一 dispatch_async(queue, ^{ NSLog(@“下载第一首歌曲!”); dispatch_group_leave(group); //ARC中此行不用写,也不能写 // dispatch_release(group); }); //5 添加任务二 dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@“下载第二首歌曲”); dispatch_group_leave(group); //ARC中此行不用写,也不能写 //dispatch_release(group); }); //6 获得调度组的通知 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@“歌曲都已经下载完毕! %@",[NSThread currentThread]); }); //7 等待调度组 监听的队列中的所有任务全部执行完毕,才会执行后续代码,会阻塞线程(很少使用) dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
六、线程管理————NSOperation
1、NSOperation的作用以及特点
(1)NSOperation是OC语言中基于GCD的面向对象的封装,NSOperation是iOS2.0推出的,iOS4之后(GCD出现)重写了NSOperation。
(2)使用起来比GCD更加简单(面向对象)。同时,苹果推荐使用,使用NSOperation不用关心线程以及线程的生命周期。
(3)提供了一些用GCD不好实现的功能,比如暂停,取消,最大并发数、依赖关系。当然GCD也有自己的特有,比如延迟、一次性执行、调度组。
(4)NSOperation是抽象类,约束子类都具有共同的属性和方法,不能直接使用,而是使用其子类。
(5)任务是并发执行的,除非遇到主队列(start方法除外)。
2、NSOperationQueue 队列
(1)两种队列
- 并发队列:程序员自己创建
- 主队列:系统创建
(2)NSOperationQueue的作用:
- NSOperation可以调用start方法来执行任务,但默认是主线程执行的。如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作。
(3)无论是使用start还是加入队列的方式来执行操作,系统都会调用NSOperation中的main方法,所以如果自定义NSOperation,就要重写此方法。
(4)添加操作到队列(主队列也一样)
- (void)addOperation:(NSOperation *)op; - (void)addOperationWithBlock:(void (^)(void))block;
(5) 其他一些常用方法和属性
- (BOOL)suspended 暂停
- (NSUInteger)operationCount 队列中的操作数
- (NSUInteger)maxConcurrentOperationCount 最大并发数
- +(NSOperation*)mainQueue 获得主队列
- +(NSOperation*)currentQueue 获得当前队列
- -(void)cancelAllOperations 取消所有操作
3、NSOperation子类——NSInvocationOperation
(1)实例
//建NSInvocationOperation对象 - (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg; //调用start方法开始执行操作,一旦执行操作,就会调用target的sel方法 - (void)start;
(2)注意:默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作。
4、NSOperation子类——NSBlockOperation
(1)实例:
//创建NSBlockOperation对象 + (id)blockOperationWithBlock:(void (^)(void))block; //通过addExecutionBlock:方法添加更多的操作 - (void)addExecutionBlock:(void (^)(void))block;
(2)注意:只要NSBlockOperation封装的操作数 > 1,就会异步执行操作。
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"%@",[NSThread currentThread]); }]; //操作添加额外的任务 [op addExecutionBlock:^{ NSLog(@"Execution %@",[NSThread currentThread]); }]; [op start]; //如果NSBlockOperation的操作数>1 开启新的线程 //这时有两个任务并发执行,一个在主线程,一个在新开启的线程
5、并发数
(1)定义:同时执行的任务数,比如,同时执行3个任务放到3个线程,并发数就是3。
(2)最大并发数及相关方法:最大并发数是系统同一时间并发执行任务的最大数。系统可以开辟多个线程、队列可以拥有多个任务,但是同时执行的任务数只能是设定好的最大并发数,直到队列中任务执行完毕。
- - (NSInteger)maxConcurrentOperationCount;
- - (void)setMaxConcurrentOperationCount:(NSInteger)cnt
(3)执行的过程
a、把操作添加到队列
b、去线程池去取空闲的线程,如果没有就创建线程
c、把操作交给从线程池中取出的线程执行
d、执行完成后,把线程再放回线程池中
e、重复b,c,d直到所有的操作都执行完
6、优先级
(1)方法一:设置NSOperation在queue中的优先级,可以改变操作的执行优先级,注意这是NSOperation中的属性,已经不推荐使用了。
- - (NSOperationQueuePriority)queuePriority;
- - (void)setQueuePriority:(NSOperationQueuePriority)p;
(2)方法二:iOS8以后推荐使用服务质量 qualityOfService属性,这是个枚举值。
(3)注意:优先级只是告诉系统:在CUP随机调度的情况下,请尽量优先调用优先级高的任务去执行,并不能绝对保证CPU全部执行优先级高的任务。
7、依赖关系
(1)定义:类似于GCD的串行队列,NSOperation之间可以设置依赖来保证执行顺序。
(2)实例
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op1 验证账号"); }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op2 扣费"); }]; NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op3 下载应用"); }]; //操作依赖 不能设置相互依赖 [op2 addDependency:op1]; [op3 addDependency:op2]; //把操作添加到队列中 waitUntilFinished是否等待这句代码执行完毕再来执行下面的代码 [self.queue addOperations:@[op2,op3] waitUntilFinished:NO]; //不同的队列之间也可以设置依赖关系 [[NSOperationQueue mainQueue]addOperation:op1];
8、监听操作的完成
(1)类似于GCD的操作组,NSOperation也可以监听操作的完成,这是NSOperation中的方法:
- - (void (^)(void))completionBlock;
- - (void)setCompletionBlock:(void (^)(void))block;
//创建操作op,任务就是循环打印输出 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 20; i++) { NSLog(@"我是op===》 %d",i); } }]; //设置操作优先级 最高的服务质量 op.qualityOfService = NSQualityOfServiceUserInteractive; //把操作添加到队列中 [self.queue addOperation:op]; //监听op的完成 [op setCompletionBlock:^{ NSLog(@"op 执行完毕了! %@",[NSThread currentThread]); }]; //创建操作op2 NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i <20; i++) { NSLog(@"我是op2+++》 %d",i); } }]; //设置操作优先级 最低的服务质量 op2.qualityOfService = NSQualityOfServiceBackground; //把操作添加到队列中 [self.queue addOperation:op2]; //本例中加入了优先级的使用:op和op2两个任务,op要优先于op2被CPU调度,理论上也是会优先于op2执行完毕,当op执行完毕时,op任务的结束被监听到,输出“执行完毕”,此时op2还在执行中。
七、线程间通信的几种方式
线程间的通讯,关键在于获得不同的线程或队列,然后在不同线程中执行任务,比如从子线程到主线程,再从主线程到子线程。
1、获得当前线程(队列)的方式
(1) NSThread管理线程: [NSThread currentThread];
(2) NSOperation管理线程: [NSOperationQueue currentQueue];
2、获得主线程(主队列)的方式
(1) NSThread管理线程: [NSThread mainThread]; 主线程
(2) NSOperation管理线程: [NSOperationQueue mainQueue]; 主队列
(3)GCD管理线程: dispatch_get_main_queue(); 主队列
3、在某线程中执行
(1)此方法是NSObject的扩展(NSThread.h中):
- - (void)performSelectorOnMainThread
(2)开启新线程执行:
- - (void)performSelectorInBackground
(3)在某个线程中执行:
- -(void)performSelector:onThread:
到此处,本文只是简单的将iOS中多线程的基本概念和使用做了简单介绍和整理,如有问题,还望多包含并指教,随后将更多讨论些在实际开发中的应用。