DSP

第十九章 并行编程

2019-07-13 12:27发布

线程是(thread)进程(process)内假象的持有CPU使用权的执行单位。
使用多线程的程序称为多线程(multithread)运行。从程序开始时就运行的线程称为主线程,除此之外,之后生成的线程称为次线程(secondary thread)或子线程(subthread)。创建线程时,创建方的线程称为父线程,被创建方的线程称为子线程。父线程和子线程并行执行各自的处理,但父线程可以等到子线程执行终止后与其会和(join)。另一方面,在线程被创建后,也可以切断父子关系指定他们不进行会和。该操作称为分离(detach)。多线程访问的变量称为共享变量(shared variable)。各个线程都分配有栈且独立进行管理,基本上不能访问其他线程的栈内变量(自动变量)。使用引用计数管理时,为了使对象之间解耦合,子线程方需要创建与父线程不同的自动释放池来管理。使用垃圾回收时不需要这样。线程安全多个线程同时操作某个实例时,如果没有得到错误结果或实例没有包含不正确的状态,那么该类就称为线程安全。(thread-safe)。结果不能保证时,则称为非线程安全或线程不安全(thread-unsafe)。需要注意的是C语言函数,就现状来看,BSD函数的大部分,例如printf()等,都不是线程安全的。注意点要使多线程程序不出错且高效执行,并行编程的知识必不可少。而且,很多多线程中遇到的问题都可以通过NSTimer类或延迟消息发送来解决。使用NSThread创建线程Foundation框架提供了NSThread类来创建并控制线程,接口在Foundation/NSThread.h中声明。创建线程需要执行下面的类方法:+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(nonnullid)target withObject:(nullableid)argument//对对象aTarget调用方法创建新线程并执行。选择器sSelector必须是仅获取一个id类型参数且返回值为void的执行方法。指定的方法执行结束后,线程也随之终止。线程从最初就被执行了分离,所以终止时没有和父线程会和。当主线程终止时,包含子线程的程序也全部随之终止。使用引用计数管理时,有时需要执行的方法自身来管理自动释放池。此外,参数aTarget和anArgument中指定的对象也与线程同时存在,即在创建线程时被保存,在线程终止时被释放。创建新线程并行执行的方法除了上述方法还有很多。其他方法参考NSThread,NSObject的参考文档。程序可以调用NSThread类方法来确认是否是多线程运行:+ (BOOL)isMultiThread//多个线程并行执行时或者只有主线程在执行时。只要在此之前创建了线程,则返回YES。当前线程一个线程称自身为当前线程(curent thread),区别于其他线程。子线程将创建时指定的方法执行完后也会随之终止,但也可以中途终止。为此,可以使用当前线程(线程自身)来执行下一个NSThread类方法。但是,使用引用计数管理时,终止前一定要释放自动释放池。+ (void)exit;使用下述方法获得表示线程的NSThread实例。+ (NSThread *)currentThread//获得表示当前线程的NSThread实例。+ (NSThread *)mainThread//获得表示主线程的NSThread实例。每个线程都可以持有一个该线程固有的NSMutableDictionary类型的字典。GUI应用和线程GUI应用中有较容易的方法来使用线程,即将GUI相关的时间处理或绘图集中在主线程中进行。使用下面的方法就可以从子线程依赖主线程中的方法处理。- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullableid)arg waitUntilDone:(BOOL)wait//选择器aSelector和参数arg中指定的方法的执行依赖于主线程。wait为YES时,当前线程会一直等待至执行结束。主线程中必须有事件循环(运行回路)。互斥需要互斥的例子?在OS的支持下,线程在运行的过程中会时而得到CPU的执行权,时而被挂起执行权,2个方法的执行情况如下所示。即使CPU的执行权被挂起,寄存器的值也仍然可以保存,所以各线程都能正常处理。但是由于线程2写入的值消失了,因此从整体上看,这偏离了我们期待的结果。原因是值的读取,更新,写入操作被多线程同时执行了。在上图中,我们将同时只可以由一个线程占有并执行的代码并执行的代码称为临界区(critical section),或称为危险区。互斥的目的就是限制可以在临界区执行的线程。为了使多个线程间可以相互排斥的使用全局变量等共享资源,可以使用NSLock类。该类的实例也就是可以调整多线程行为的信号量(semaphore)。Cocoa环境中也称为锁。获得锁称为加锁,释放锁称为解锁。锁应该在程序开始在多线程执行前创建。NSLock *countLock = [[NSLock alloc] init];获得锁的方法和释放锁的方法都在协议NSLocking中定义。- (void)lock//如果锁正被使用,则线程进入休眠状态。如果锁没有被使用,则将锁的状态变为正被使用,线程继续执行。- (void)unlock//将锁置为没有在被使用,此时如果有等待该锁资源的正在休眠的线程,则将其唤醒。某个锁被执行后,必须执行一次unlock。下面来看另外一个使用锁的例子。考虑一下全局变量值自增时返回其结果的方法。多线程执行时,全局变量theCount若想正确的自增,就需要使用锁countLock来管理。- (int)inc {[countLock lock];++theCount;[countLock unlock];returntheCount;}咋一看好像没问题,但从释放锁到返回值期间,其他线程可能会修改变量值。- (int)inc {inttmp;[countLock lock];tmp = ++theCount;[countLock unlock];returntmp;}各线程持有独立的栈,自动变量tmp可以在整个线程中局部利用,而且无需担心被其他线程访问。死锁死锁就是多线程永远在等待一个不可能实现的条件而无法继续执行。尝试获得锁- (BOOL)tryLock//用接收器尝试获得某个锁,如果可以获得该锁则返回YES。不能或得时与lock处理不同,线程没有进入休眠状态,而是直接返回NO并继续执行。该方法十分便利,但要确保只能在可以获得锁时才执行unlock,创建程序时必须注意这一点。条件锁类NSConditionLock称为条件锁。该锁持有整数值,根据该值可以获得锁或者等待。由于NSConditionLock实例可以持有的状态为整数型,所以事先用枚举常数或宏定义就可以了。如果只用0或1,不仅不容易理解,也可能造成错误。NSRecursiveLock[aLock lock];[aLock lock];//这里发生死锁[aLock unlock];[aLock unlock];使用NSRecursiveLock类的锁,拥有锁的线程即使多次获得同一个锁也不会进入死锁。NSRecursiveLock类的锁使用起来十分方便,但排除被重复加锁的情况,用NSLock来重新记述的话,性能会更好。@synchronized程序内的块可以指定为不被多线程同时使用。为此可以使用@synchronized编译符,如下所示:@synchronized(obj) {//记述想要排斥地执行的内容}通过使用该段代码,运行时系统就可以创建排斥地执行该代码块的锁(mutex)。参数obj通常指定为该互斥锁要保护的对象。obj自己不需要是锁对象。线程如果要执行该代码块,首先会尝试获得锁,如果能获得锁则可以执行块内代码。块执行结束时一并释放锁。使用break或return从块内跳出到块外时也被视为块执行终止。而且,在块内发生异常时,运行时系统会捕捉异常并释放块。@synchronized的参数对象决定对应的块。所以,同一个参数的@synchronized块如果有多个,则不可以同时执行。根据参数选择的方法不同,@synchronized会在并行执行的受限对象和可以执行的普通对象之间动态切换。@synchronized与上述NSRecursiveLock类的锁一样,可以递归调用。使用@synchronized块时,加锁和解锁必须成对进行,因此可以防止加锁后忘记解锁这种问题的发生。操作对象和并行处理新的并行处理程序Mac OS10.6及iOS4.0之后,导入了可以使系统全体线程更高效运行,并且使并行处理应用更易开发的架构,称为大中央调度(GCD,Grand Central Dispatch)。在使用GCD的程序中,可以将想要提高处理速度的部分分解为非同步执行的多个任务,并将这些任务放入到等待队列(queue)中。虽然必须指定任务间的依赖关系(前后关系),但却不需要关心线程的创建和调度。然后,CPU内核数以及其他应用的要求都会被动态的判断,由系统决定最佳的并行程度和高效的调度方式并行。GCD的核心是用C语言写的系统服务,应用将应该执行的各种任务封装成块对象写入到队列中。通过对比各任务启动线程的代价,该操作可以非常轻量地运行。为了直接使用GCD的功能,必须要使用C语言函数,然而,在Objective-C中,通过用NSOperation类来表示任务,并将其追加到NSOperationQueue类队列中,即可实现并行处理。使用NSOperation的处理概述和将NSArray实例称为数组对象一样,下面的NSOperation实例也可以称为操作对象(operation object),或简称为操作。操作对象可以将需要执行的代码和相关数据集成并封装。操作对象放在队列中等待被调用,线程放在线程池中被管理。操作对象也可以像“任务A完成后调用任务B”,这样指定调用的先后关系。此时,任务A依赖任务B,这种关系被称为依赖关系(dependency)。而且,还可以设定操作的优先权(priority)。为操作提供等待队列功能的是NSOperationQueue。该类的实例被称为操作队列,或简称为队列。加入到队列的操作在执行之前也可以取消。操作对象可以直接调用start方法来执行任务。但是为了能并行执行,标准做法是在start内创建线程。使用NSOperation和NSOperationQueue的简单用法NSOperation提高了处理任务的功能,它本身又是抽象类。所以就需要先定义子类,然后在子类在定义必要的处理。但是,由于操作对象可以被多个线程同时访问,因此子类方法必须是线程安全的。NSOperation的子类必须将要执行的任务写入到下面的main方法中。由于在NSOperation中定义的方法不发挥任何作用,所以没必要调用super。- (void)mainNSOperation子类至少应该包含main方法。此方法应该按照如下方式定义。利用垃圾回收机制时不需要自动释放池,只需启动垃圾回收器即可。另外,@catch后的括号内并不表示省略的意思,而是就应该写成“…”- (void)main {@try{@autoreleasepool{//在这里书写任务中需要进行的处理}}@catch(...) {//再次抛出异常,不能使用@throw等向外部传播。}}使用引用计数方式进行内存管理时,程序会一直保持着添加到队列中的操作对象。使用垃圾回收时,因为队列需要访问操作对象,所以直到执行终止操作对象才会被释放。创建的队列即使被多个线程同时操作也不会出任何问题,因此并不需要为互斥加锁,或者用@syncronized块来保护。一个程序可以创建多个队列,但是,一个操作对象一次只能加入到一个队列中。而且已经执行完或正在执行的操作对象都不可以加入到队列中。否则就会引发异常。操作对象一旦加入到队列,就不能从队列中消除,只能取消。等待至聚合任务终止NSInvocationOperation的使用方法在Cocoa框架中,NSOperation的子类包括NSInvocation和NSBlockOperation。使用这些类时,即使不定义子类也能创建操作对象。NSBlockOperation的使用方法该方法使用块对象构造任务。NSBlockOperation中添加多个对象NSBlockOperation中包含数组对象,里面保存着多个块对象,而且这些块对象可以被当作任务执行。当有多个块对象时(如果可能的话),这些块对象就会被并行执行。设置任务间的依赖当存在多个任务时,有时我们会需要指定一定的顺序关系,在执行某个任务后继续执行另一个任务。在多任务中,某些任务有依赖关系,而其他任务可以并行处理。依赖关系不能形成闭环。闭环关系中所有操作都不能被执行。任务的优先级设置在多个任务中,既有需要比其他任务优先执行的任务,也有非优先执行的任务,此时可以使用NSOperation提供的优先级的设定方法。但是,并不能保证高优先级的任务一定会优先于低优先级的任务执行。设定最大并行任务数操作队列可以同时启动多个任务的情况下,可以设定最大可以并行执行的任务数。一般情况下,任务的并行执行可以根据系统状况判断,选择该状态时,指定常数值NSOperationQueueDefaultMaxConcurrentOperationCount。该值为确定值(实际值为-1)。即使指定了最小值为1,也不一定会按照加入到队列是的顺序来处理任务。而且,指定较大的数字也不会执行更高速。终止任务取消队列中操作对象的任务。在任务执行的过程过程中接收到cancel时,为了转移到中断处理,任务内容必须按如下方式编程实现。具体来说,程序在main方法的开头及处理途中的各个重要的地方检查自己的isCancelled值,如果返回YES则进行中断处理。设置队列调度为中断状态即使为中断状态,也可以向队列中添加操作,而且即使处于中断状态,也不会停止正在执行的任务。使用连接的通信在Mac OS的Foundation框架中,为了使不同的线程或进程可以双工通信,提供了类NSConnection。NSConnection对象除了被作为线程间的安全通信线路使用外,也提供了创建应用间所使用的分布式对象(distributed object)的方法。而iOS中则不提供该方法。