很多朋友都说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(区别于CFRunloopRef)
(1)功能作用:运行循环,又叫消息循环或事件循环。
- 检测、接收 “输入事件” 并执行。
- 保证程序不退出(主线程)。
- 如果没有事件发生,会让程序进入休眠状态。
(2)特点:
- NSRunLoop不能单独存在,必须存在于线程中,不论主线程中还是子线程中都有一个消息循环。
- 主线程的RunLoop是默认开启的,子线程中的RunLoop默认不开启(使用run方法开启)。
- 只要线程一启动,内部就会有一个默认的主RunLoop,而每一个App,只要一启动,就会自动有一个主线程。
(3)(两大核心之一)输入事件:
- 输入源:比如 键盘输入,滚动scrollView,performSelector方法
- 定时源:NSTimer 定时器
(4)(两大核心之二)运行模式(消息循环模式):
- 线程的消息循环运行在某一种消息循环模式上。
- 输入事件必须设置消息循环的运行模式,并且如果想让输入事件可以在消息循环上执行,输入事件的消息循环运行模式必须(设置成)和当前消息循环的运行模式一致
- 几种常用的运行模式:
(1)NSDefaultRunLoopMode:默认的运行模式,用于大部分操作,除了NSConnection对象事件。
(2)NSEventTrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动)。
(3)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];
- 常见问题:滑动ScrollView时的流畅问题
NSTimer* timer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(readTimer) userInfo:nil repeats:YES];
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(readTimer) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
同理,在进行网络请求时,返回参数处理也默认使用NSDefaultRunLoopMode模式,也会出现滚动scrollview时,出现数据解析异常,但是AFNetworking框架中对返回参数的处理默认使用NSRunLoopCommonModes,所以不用担心这个问题。(这种说法,在NSURLConnection还没有废弃之前)。
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_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);
4、栅栏函数 dispatch_barrier_async
(1)定义:类似调度组,实现任务的按顺序执行(给任务们之间加上栅栏),也有同步方法。
(2)实现:
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^(){ NSLog(@"dispatch-1"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"dispatch-2"); }); dispatch_barrier_async(concurrentQueue, ^(){ NSLog(@"dispatch-barrier"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"dispatch-3"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"dispatch-4"); });
现象:(1)先执行: dispatch-1,dispatch-2 (2) 然后执行dispatch_barrier_async中的操作,(现在就只会执行这一个操作)执行完成后,即输出dispatch-barrier (3)最后该并行队列恢复原有执行状态,继续并行执行dispatch-3,dispatch-4
六、线程管理————NSOperation
1、NSOperation的作用以及特点
(1)NSOperation是OC语言中基于GCD的面向对象的封装,NSOperation是iOS2.0推出的,iOS4之后(GCD出现)重写了NSOperation。
(2)使用起来比GCD更加简单(面向对象)。同时,苹果推荐使用,使用NSOperation不用关心线程以及线程的生命周期。
(3)提供了一些用GCD不好实现的功能,比如暂停,取消,最大并发数、依赖关系、支持KVO。当然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中多线程的基本概念和使用做了简单介绍和整理,如有问题,还望多包含并指教,随后将更多讨论些在实际开发中的应用。