多线程
基本概念:
1、进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程有3个特性:
. 独立性
. 动态性
. 并发性
并发性和并行性是两个概念,并行是指在同一时刻,有多条指令在多个处理器上同时执行;并发是指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上有多个进程同时执行的效果。
2、线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成
归纳起来可以这样说:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
3、多线程编程的优点:
>>1、进程间不能共享内存,但线程之间共享内存非常容易
>>2、系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多线程的效率高
>>3、iOS提供了多线程实现方式,从而简化了iOS的多线程编程
4、同步和异步
同步:同步就是顺序执行,通常跟串行并在一起。同步访问的时候,一个线程在访问的时候另外一个线程不能访问
异步:异步就是可以同时执行,通常跟并行并在一起。异步访问的时候,一个线程访问的时候另外一个程序也能同时访问。
iOS提供了如下3中多线程编程的技术:
>>1、使用NSThread实现多线程
>>2、使用NSOperation与NSOperationQueue实现多线程
>>3、使用GCD(GrandCentral Dispatch)实现多线程
一、使用NSThread实现多线程
1、创建和启动线程
创建NSThread有两种方式:
. - (id) initWithTarget:self selector:@selector(run) object:nil
. [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
上面两种方式的本质都只是将target对象的两种方法转换为线程执行体,其中selector方法最多可以接收一个参数,而object参数就代表传给selector的参数。
null.png注意:在使用多线程编程时不要忘了Objective-C程序运行时默认的是UI线程。
- currentThread:该方法是NSThread类的类方法,该方法总是返回当前正在执行的线程对象。除此之外,程序还可以通过setName方法为线程设置名字,也可以通过name方法返回指定线程的名字。
2、线程状态
当线程创建并启动以后,它既不是已启动就进入执行状态也,不是一直处于执行状态,即使线程开始运行以后,他也不可能一直“霸占”CPU肚子运行,线程状态也会多次在运行、就绪状态之间切换。线程在创建之后系统为其分配了资源,调用了start方法之后就进入就绪状态,至于该线程核实开始执行取决于系统的调度。
小技巧:如果程序希望调用子线程的start方法之后子线程立即执行,程序可以使用:[NSThread sleepForTimeInterval:o.o1]; 让主线程休眠一毫秒
3、终止子线程
(1)、线程会以以下3中方式之一结束,结束后就处于死亡状态
>>1、线程执行体执行完成,线程正常结束
>>2、线程执行过程中出现了错误
>>3、直接调用NSThread类的exit方法来终止当前正在执行的线程
(2)、测试线程是否正在运行,可以使用线程对象的isExecuting、isFinished方法,当线程正在执行的时候isExecuting方法会返回YES;当线程执行结束的时候isFinished放就会返回YES。
如果希望在UI线程中终止子线程,NSThread并没有提供方法来终止某个子线程。为了在UI线程中终止子线程,可以向子线程发送一个信号(比如调用子线程的cancel方法),然后在子线程的的线程执行体方法中进行判断,如果子线程收到过终止信号,程序应该调用NSThread的exit方法来终止当前正在执行的循环。
屏幕快照 2016-04-29 下午2.53.07.png4、线程睡眠
(1)、如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过NSThread的类方法sleepXxx方法来完成:
. + (void) sleepUntilDate:(NSDate *)aDate:让当前正在执行的线程暂停到指定的时间点,并进入阻塞状态
. + (void)sleepForTimeInterval:(NSTimeInterval) time: 让当前正在执行的线程暂停time秒,并进入阻塞状态。
屏幕快照 2016-04-29 下午2.54.02.png
5、线程优先级
(1)每一个线程都有它的优先级,优先级高的线程获得较多的执行机会,而优先级较低的线程获得较少的执行机会。每个子线程的默认优先级都是0.5。优先级的范围是0.0~1.0的。
(2)NSThread提供了如下实例方法和类方法来设置获取线程的优先级:
>>1、+ threadPriority: 该类方法获取当前正在执行的线程的优先级
>>2、- threadPriority: 该实例方法获取当前正在执行的线程的优先级
>>3、+ setThreadPriority: 该类方法设置当前正在执行的线程的优先级
>>4、- setThreadPriority: 该实例方法设置该方法的线程对象的优先级
二、线程同步与线程通信
1、多线程编程容易出现错误情况,这是由于系统的线程调度具有一定的随机性造成的。不过,即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易偶然出现线程安全问题
例子:模拟银行取钱过程
>>1、定义一个账户类
.h
.m
5.png 1.png在控制器中
屏幕快照 25.png
多次运行上程序,很有可能看到如图结果:
25.png
按常理,应该是第一个线程可以取钱,第二个线程显示“余额不足”。以上结果就出现了线程安全问题。如果将Accou.m中注释掉的那行代码取消注释,将永远出现上图结果。
2、上程序之所以会出现这样子的错误,是因为线程执行体的方法不具备同步安全性——程序中有两个并发程序在修改同一个Accoun对象。而且系统恰好在注释代码处执行切换,切换给另一个线程去修改Accoun对象,所以就出问题了。
(1)为了解决这个问题,objec-c的多线程支持引入了同步,使用同步的通用方法就是@synchronized修饰代码块,被@synchronized袖上衣的代码块课简称为同步代码块。同步代码块的语法格式:
@synchronized (obj)
{
…...
//此处的代码就是同步代码块
}
obj是同步监视器。上面代码的含义是:在线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
注意:任何时候,只有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定
(2)虽然object-c允许使用任何对象作为同步监视器,但想一下同步监视器的目的—阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源作为同步监视器。对于以上程序,应考虑使用账户对象作为同步监视器。为此我们只需要修改Accou类的draw方法:
12314314.png
修改后得到了先要的结果:
1434647.png(3)任何线程在修改指定资源之前,首先都要对资源进行加锁,在加锁期间,其它资源无法修改该资源,当该线程修改完成后,释放对该资源的锁定。通过这种方式就可包拯并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全。
(4)线程安全的类具有如下的特征:
>>1、该类的对象可以被多个线程安全地访问
>>2、每个线程调用该对象的任意方法之后都将得到正确的结果
>>3、每个线程调用该对象的任意方法之后,该对象依然保持合理状态
Foudation框架中有可变和不可变版本,如NSArray是不可变的,NSMutableArray是可变的,不可变的类是线程安全的,可变的类是线程不安全的。可变类的对象需要额外的方法来保证其线程的安全。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采取如下的策略:
>>1、不要对线程安全类的所有方法都进行同步,只有对那些会改变竞争资源(共享资源)的方法进行同步
>>2、如果可变类有两种运行环境:单线程环境和多线程环境,则应该为可变了提供两种版本——线程不安全和线程安全版本。在单线程版本中使用线程不安全版本以保证性能,在多线程版本中使用线程安全。
3、释放对同步检修器的锁定
获得对同步监视器的锁定后,何时会释放对同步监视器的锁定呢?程序是无法显示释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
>>1、当前线程的同步代码块执行那个结束,当前线程即释放同步监视器
>>2、当前线程在同步代码块中遇到goto、return终止了该代码块、该方法的继续执行时,当前线程会释放同步监视器
>>3、当前线程在同步代码块中出现了错误,导致改代码块一场结束时,将会释放同步监视器。
4、同步锁(NSLock)
(1)NSLock是控制多个线程对共享资源进行访问的工具。通常锁提供了对共享资源的独占访问,每次只能有一个线程对NSLock对象加锁,线程开始访问共享资源之前应先获得NSLock对象。NSLock可以显示地加锁、释放锁。对于如上的取钱程序只需做如下修改:
234346356.png
123467.png
(2)NSLock同样符合 “加锁 -> 修改 -> 释放锁” 的操作模式,而且同样能够保证同一时刻只能有一个线程能进入临界区。
5、使用NSCondition控制线程通信
(1)线程的调度具有一定的透明性,但是程序通常无法准确控制线程的轮换执行,,当我们可以通过一些机制来保证线程协调运行,也就是处理线程之间的通信。NScondition类可以处理线程间的通信,其实现了NSLocking协议,因此也可以调用lock、unlock来实现线程的同步。除此之外NSCondition可以让那些已经锁定NSCondition对象却无法继续执行的线程释放NSCondition对象,NSCondition也可以唤醒其它处于等待状态的线程
(2)NSCondition提供了如下3个方法来处理线程:
>>1、wait:该方法导致当前线程一直等待,直到其它线程调用该NSCondition的signal或broadcast方法来唤醒该线程。wait方法有一个变体:- (BOOL) waitUntilDate: (NSDate *) limiteout, 用于控制等待到指定时间点,如果到了该时间点,该线程会自动被唤醒
>>2、- signal:唤醒在次NSCondition对象上等待的单个线程。如果所有线程都在该对象上等待,则会选择唤醒其中一个线程。选择是任意的。只有当前线程放弃对该NSCondition对象的锁定后(使用wait方法),次啊可以执行被唤醒的线程。
>>3、- broadcast:唤醒再次NSCondition对象上的所有线程。只有当前线程放弃对该NSCondition对象的锁定后,才可以执行被唤醒的线程。
例子:生产者、消费者
生产者在生产的同时消费者不能消费;消费者在消费的同时生产者也不能生产。
账户类:
. h
3.png
.m
2.png 34ss34.png 1312sggj.png 12rgfh34tsfdq32.png
三、使用GCD实现多线程
1、使用NSThread实现多线程编程比较复杂,需要程序员自己控制多线程的同步、并发,稍不小心,多线程就会出现错误。为了简化多线程的运用开发,iOS提供了GCD来实现多线程。
(1)GCD的两个核心概念:
>>1、队列:队列负责管理开发者提交的任务,GCD队列始终以FIFO的方式来处理任务。该队列既可以是串行的,也可以是并行的。队列底层会维护一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。串行队列底层的线程池只要维护一个线程即可,并行队列底层的线程池需要维护多个线程。
>>2、任务:任务就是用户提交给队列的工作单元,这些任务将会提交给队列底层维护的线程池执行,因此这些任务会以多线程的方式执行。
(2)使用GCD多线程编程要遵守两个步骤即可:
>>1、创建队列
>>2、将任务提交给队列
2、创建队列
(1)GCD的队列可分为两种:串行队列和并发队列
(2)如下函数用于创建或访问队列:
3264923sdnlkf.png
如果将多个任务提交给并发队列,并发队列可按照FIFO的顺序启动多个并发执行的任务,由于任务的耗时长短不同,所以后提交的任务完全可能先完成。
3、异步提交任务
(1)iOS提供了如下函数来向队列提交任务,下面这些函数很多都有两个版本:一个接收代码块作为参数的版本,一个接收函数作为参数的版本。其中接收函数作为参数的函数名随后多了后缀_f,而且会多一个参数,用于向函数传入应用程序定义的上下文。
1231sdf.png 124134htgdb.png.h
312314fadfsy565.png
.m
121789difa8129784.png124135946357892.png
(2)使用GCD下载图片
13243537578.png
4、同步提交任务
(1)dispatch_sync()函数以同步的方式提交代码,该函数必须等到代码块执行完成后返回。如果程序提交了两个代码块(即使提交给并发队列),也必须等到第一个任务完成后才能执行第二个任务。
例子:
1242365868432.png
dipatch_sync()函数虽然启动另外的线程来执行代码块,但它依然会阻塞主线程。
以上实例虽然将代码块提交给并发队列,但由于采用的是同步的方式,因此必须等到第一个代码块执行完成后才会提交第二个代码块。实际上是先后提交两个代码块,因此程序只需要使用一个线程。
5、多次执行的任务
(1)dispatch_apply()函数将控制器提交的代码块重复执行多次,如果改代码块被提交给并发队列,系统可以使用多个线程并发执行同一个代码块。
例子:
312748298.png
结果:
屏幕快照 2016-04-29 下午3.22.46.png
由于程序将代码块提交给并发队列,因此程序启动了5个线程来重复执行该代码块。
6、只执行一次的任务
(1)sipatch_once()函数将控制提交的代码块在整个应用程序的生命周期内最多只执行一次,而且该函数无需传入队列,这意味着系统将直接使用主线程执行该函数提交的代码块。该函数在执行的过程中,传入了一个dispatch_once_t(本质就是long类型)类型的指针,用于判断改代码块是否已执行过。
2032327.png第一次执行的时候,按钮持续高亮3秒,再次点击的时候就不会再持续高亮。
四、后台运行
所有应用进入后台之前都要做以下事情
>>释放所有可以释放的内存
>>保存用户数据或状态信息。因为程序在进入后台之后可能会被杀死
1、进入后台时释放内存
应用进入后天时,iOS系统会优先终止那些占用内存大的应用。如果应用尽可能释放其所占用的内存,那么应用就可以在后台存活更久。可以得出的一个结论是:应用暂停时所占用的内存越少,iOS彻底终止该应用的风险就越低。
在非ARC中,程序进入后台时,将需要释放掉资源的引用计数器变为0;
在ARC环境下,程序进入后台时,将需要释放资源的变量赋为nil即可。
在viewDidLoad中监听应用进入前后台
211353.png
进入前台操作
12913840fsd.png
进入后台操作
1242385028746.png
2、进入后台时保存状态
进入后台后,应用的一些数据需要保存
1324435647657687.png
程序从后台转入前台时读取状态数据
12456567.png
3、请求更多的后台时间
当应用进入后台后,不要在主线程中执行超过5秒的任务,如果应用进入后台花费了太多的时间(即applicationDidEnterBackground:方法的执行体花太多费的时间),应用程序可能从内存中被删除。例如:下载和传输任务,在进入后台时,不要强制在applicationDidEnterBackground:方法中直接完成该任务。系统会因为应用进入后台花费太多时间而删除该应用。正确的做法是:以applicationDidEnterBackground:方法为平台,告诉系统进入后台后还有更多的任务需要完成,从而向系统申请更多的后台时间。
请求更多的后台时间的步骤:
>>1 、调用UIApplication对象的beginBackgroundTaskWithExpirationHandler:方法请求更多的后台执行时间,该方法默认请求额外获得10钟后台时间。该方法需要传入一个代码块作为参数,如果请求获取后台时间失败,将会执行该代码块。该方法将会返回一个UIBackgroundTaskIdentifier类型的变量,该变量可作为后台任务的标识符。
>>2、调用dispatch_sync()方法将指定代码块提交给后台执行
>>3、后台任务执行完成时,调用UIApplication对象的endBackgroundTask:方法结束后台任务。
214235.png
1242.png
五、使用NSOperation与NSOperationQueue实现多线程
1、NSOperationQueue:代表一个FIFO的队列,它负责管理系统提交的多个NSOperation,它的底层维护一个线程池,会按顺序启动线程来执行提交给该队列的NSOperation任务。
NSOperation:代表一个多线程任务。NSOperation还有NSInvocationOperation和NSBlockOperation两个子烈。NSOperation有两个使用方式:
>>开发者实现NSOperation的子类
>>开发者直接使用NSInvocationOperation或NSBlockOperation子类
2、使用NSOperation与NSOperationQueue实现多线程的步骤:
>>1、创建NSOperationQueue队列,并为该队列设置相关的属性。
>>2、创建NSOperation子类的对象,并将该对象提交给NSOperationQueue队列,该队列将会按顺序依次启动每个NSOperating。
开发者提交的NSOperation是由NSOperationQueue维护的线程池中的线程负责执行,NSOperationQueue提供了如下常用方法:
2144762312.png 356231.png3、使用NSInvocationOperation和NSBlockOperation
用于封装需要异步执行的任务,它们在用法上非常相似,区别是NSInvocationOperation用于将特定对象的特斯丁方法封装成NSOperation,而NSBlockOperation则用于将代码块封装成NSOperation。
例子:下载图片
12467643524.png
4、定义NSOperation子类
NSOperation不会直接拿来使用,而是选择创建它的子类,创建NSOperation的子类需要重写一个方法:- (void) mian,该方法的方法体将作为NSOperationQueue完成的任务。
创建NSOperation的子类
12467441224.png 8987856354.png控制器实现部分代码:
1243464212.png