欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Objective-C编程 — 并行编程

程序员文章站 2022-11-21 16:30:55
多线程 线程的基本概念 线程 (thread)是进程(process)A 内假想的持有 CPU 使用权的执行单位。一般情况下,一个进程 只有一个线程,但也可以创建多个线程并在进程中并行执行。应用在执行某一处理的同时,还可以 接收 GUI 的输入。 使用多线程的程序称为 多线程 (multithrea ......

多线程

线程的基本概念

线程 (thread)是进程(process)a 内假想的持有 cpu 使用权的执行单位。一般情况下,一个进程 只有一个线程,但也可以创建多个线程并在进程中并行执行。应用在执行某一处理的同时,还可以 接收 gui 的输入。

使用多线程的程序称为 多线程 (multithread)运行。从程序开始执行时就运行的线程称为 主线程 , 除此之外,之后生成的线程称为次线程(secondary thread)或子线程(subthread)。

创建线程时,创建方的线程为父线程,被创建方的线程为子线程。父线程和子线程并行执行各
自的处理,但父线程可以等到子线程执行终止后与其会合(join)。而另一方面,在线程被创建后, 也可以切断父子关系指定它们不进行会合。该操作称为 分离 (detach)。这里所说的 nsthread 就是在 分离状态下创建线程。

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的ios交流群:1012951431, 分享bat,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

由于被创建的线程共享进程的地址空间,所以能够*访问进程的空间变量。多线程访问的变量称为 共享变量 (shared variable) 。共享变量大多为全局变量或静态变量,但因为地址空间是共享的, 所以理论上所有内存区域都可以称为共享变量。

如果多线程胡乱访问共享变量,那么就不能保证变量值的正确性。所以有时就需要按照一定的 规则使多线程可以协调动作。此时就必须执行线程间 互斥 (或者排他控制,mutual exclusion)(见 。 各个线程都分配有栈且独立进行管理。基本上不能访问其他线程的栈内的变量(自动变量)。通 过遵守这样的编程方式,就可以*访问方法或函数的自动变量,而且不用担心互斥。

使用引用计数管理方式时,为了使对象之间解耦合,子线程方需要创建与父线程不同的自动释
放池来管理。使用垃圾回收时不需要这样。

a 任务(task)这一名称也被用来表示与进程同样的概念,在苹果公司的文档“multithreading programming topics”中, 可以包含多线程的程序的执行单元称为进程,而任务则被用来抽象地表示应该进行的作业。

线程安全

多个线程同时操作某个实例时,如果没有得到错误结果或实例没有包含不正确的状态,那么该 类就称为 线程安全 (thread-safe)。结果不能保证时,则称为非线程安全或线程不安全(thread-unsafe)。

一般情况下,常数对象是线程安全的,变量对象不是线程安全的。常数对象可以在线程间安全
地传递,但对变量对象共享时,需要恰当地执行互斥或同步切换。

需要注意的是 c 语言的函数。就现状来看,bsd 函数的大部分,例如 printf() 等,都不是线程安 全的。

注意点

在某些情况下,使用多线程可以使处理高速化、实现易于使用的接口、使实现更简单等。但并 不是说使用多线程后就一定会得到这些优点。

要想使多线程程序不出错且高效执行,并行编程的知识必不可少。线程间的任务分配和信息交 换、共享资源的互斥、与 gui 的交互及动画显示等,在使用时都要特别小心。

一般情况下,自己实现多线程程序是很困难的,而且也容易埋下高隐患。稍有差错或设计失误,
多 线 程 便 不 能 发 挥 效 果, 甚 至 还 会 导 致 未 知 原 因 的 释 放 或 异 常 终 止。 使 用 19.3 节 中 介 绍 的 nsoperation,虽然可以较容易地实现多线程程序,但是也必须掌握线程动作、互斥等相关知识。不 能适应这些的读者建议去参考一下并行编程的相关书籍。

而且,很多多线程中遇见的问题都可以通过 nstimer 类或延迟消息发送(参考 15.1 节)来解决。 大家也不妨尝试一下用这些方法来解决相关问题。

使用 nsthread创建线程

foundation 框架中提供了 nsthread 类来创建并控制线程。该类的接口在 foundation/nsthread.h 中声明。

创建新线程需要执行下面的类方法。

+ (void) detachnewthreadselector: (sel) aselector

totarget: (id) atarget

withobject: (id) anargument

对 对象 atarget 调用方法创建新线程并执行。选择器 aselector 必须是仅获取一个 id 类型参数且返回值 为 void 的执行方法(参考 8.2 节)。

指定的方法执行结束后,线程也随之终止。线程从最初就被执行了分离,所以终止时没有和父 线程会合。当主线程终止时,包含子线程的程序也全部随之终止。

使用引用计数管理(手动及 arc)时,有时需要执行的方法自身来管理自动释放池。此外,参 数 atarget 和 anargument 中指定的对象也与线程同时存在,即在创建线程时被保存,在线程终止时 被释放。 使用下述的 nsapplication 类中的方法也能创建线程。该方法使用上面的方法,而且在使用引用 计数管理时还会创建线程的自动释放池。

+ (void) detachdrawingthread: (sel) selector

totarget: (id) target

withobject: (id) argument

创建新线程并执行的方法除了上述方法还有很多,本书中不再一一介绍。其他方法请参考 nsthread、nsobject 的参考文档。

程序可以调用 nsthread 类方法来确认是否是多线程运行。 + (bool) ismultithreaded   多个线程并行执行时或者只有主线程在执行时,只要在此之前已经创建了线程,则返回 yes。

当前线程

一个线程称自身为 当前线程 (current thread),区别于其他线程。

子线程将创建时指定的方法执行完后也会随之终止,但也可以中途终止。为此,可以使用当前 线程(线程自身)来执行下一个 nsthread 类方法。但是,使用引用计数管理时,终止前一定要释放 自动释放池。 + (void) exit

使用下述方法获得表示线程的 nsthread 实例。

+ (nsthread *) currentthread

获 得表示当前线程的 nsthread 实例。

+ (nsthread *) mainthread

获 得表示主线程的nsthread实例。查看当前线程是否为主线程时,可以使用类方法ismainthread 。 每个线程都可以持有一个该线程固有的 nsmutabledictionary 类型的字典。向 nsthread 实例发 送下面的消息类就可以取得字典。

- (nsmutabledictionary *) threaddictionary

可以使当前线程仅被中断几秒。为此,可在当前线程中执行下面的类方法。参数为实数。 + (void) sleepfortimeinterval: (nstimeinterval) ti

也可以使线程在某一时刻前中断,这时可采用下面的类方法。参数是表示日期的类 nsdate 实例。 + (void) sleepuntildate:(nsdate *) adate

如果要使线程到某个条件成立前一直保持休眠状态,则要使用下一章节介绍的锁。

gui应用和线程

在使用 gui 的应用中,事件处理和绘图等大部分处理中线程都发挥了重要作用。也可以在子线 程中创建窗体,或分担部分绘图功能,但要注意避免竞争或内存泄漏。详情请参考相关文档。

gui 应用中有较容易的方法来使用线程,即将 gui 相关的时间处理或绘图集中在主线程中进行。
使用下面的方法,就可以从子线程依赖主线程中的方法处理。该方法为 nsojbect 的范畴,在头文件 foundation/nsthread.h 中声明。

- (void) performselectoronmainthread: (sel) aselector

withobject: (id) arg

waituntildone: (bool) wait

选 择器 aselector 和参数 arg 中指定的方法的执行依赖于主线程。wait 为 yes 时,当前线程会一直等待 至执行结束。主线程中必须有事件循环(运行回路)。

互斥

需要互斥的例子

在多线程环境中,无论哪个函数或方法都可以在多线程中同时执行。但是,在使用共享变量时, 或者在执行文件输出或绘图等的情况下,多线程同时执行就可能得到奇怪的结果。

例如,使用整数全局变量 totalnumber 来累加所处理的数据的个数。为了执行下面的加法计算, 

在多线程环境中执行该方法会得到什么结果呢?

- (void)addnumber:(nsingeger)n

{

totalnumber += n;

} 在 os 功能支持下,线程在运行的过程中会时而得到 cpu 的执行权,时而被挂起执行权,2 个 方法的执行情况如图 19-1 中所示。在该图中,线程 1 将新计算的值保存在寄存器时挂起 cpu 执行 权,同时线程 2 开始执行方法。即使 cpu 的执行权被挂起,寄存器的值也仍然可以被保存,所以各 线程都能正常处理。但是,由于线程 2 写入的值消失了,因此整体上看,这偏离了我们期待的结果。 原因是值的读取、更新、写入操作被多线程同时执行了。

 
Objective-C编程 — 并行编程

在图 19-1 的例子中,我们将同时只可以由一个线程占有并执行的代码部分称为临界区(critical section),或称为危险区。互斥的目的就是限制可以在临界区执行的线程。

为了使多个线程间可以相互排斥地使用全局变量等共享资源,可以使用nslock 类。该类的实例 也就是可以调整多线程行为的 信号量 (semaphore)或者 互斥型信号量 (mutual exclusion semaphore)。 cocoa 环境中也称为 锁 (lock)。

锁具有每次只允许单个线程获得并使用的性质。获得锁称为“加锁”,释放锁称为“解锁”。

锁和普通的实例一样,使用类方法alloc 和初始化器init 来创建并初始化。但是,锁应该在程 序开始在多线程执行前创建。

nslock *countlock = [[nslock alloc] init];

获得锁的方法和释放(unlock)锁的方法都在协议 nslocking 中定义。

- (void) lock   如果锁正被使用,则线程进入休眠状态。

如果锁没有被使用,则将锁的状态变为正被使用,线程继续执行。

- (void) unlock   将 锁置为没有在被使用,此时如果有等待该锁资源的正在休眠的线程,则将其唤醒。

在上例中,使用锁后会产生如下效果。但需要预先创建 nslock 的实例 alock。在该代码中,从 某线程执行 a 取得锁到该线程执行 b 释放锁期间,其他线程在执行 a 时将进入休眠状态,不能执 行临界区代码。锁被释放后,在执行 a 时休眠的线程中选择一个线程,该线程在取得锁后进入临界 区执行。

- (void)addnumber:(nsingeger)n {     [alock lock];  ─────────────────────────────────────────  a     totalnumber += n;    // 临界区     [alock unlock]; ────────────────────────────────────────  b }

某个锁被lock 后,必须执行一次unlock 。而且lock 和unlock 必须在同一个线程执行 a。

下面来看另外一个使用锁的例子。考虑一下全局变量值自增时返回其结果的方法。多线程执行 时,全局变量 thecount 若想正确地自增,就需要使用锁 countlock 来管理。

可以采用如下定义。 

 
Objective-C编程 — 并行编程

a lock 和 unlock 必须在同一个线程中执行,因为 nslock 是基于 posix 线程实现的。

死锁

线程和锁的关系必须在设计之初就经过仔细的考虑。如果错误地使用锁,不但不能按照预期执 行互斥,还可能使多个线程陷入到不能执行的状态,即死锁(deadlock)状态。

死锁就是多线程(或进程)永远在等待一个不可能实现的条件而无法继续执行,如图 19-2 所示。 

 
Objective-C编程 — 并行编程

线程 1 占有文件 a 并正在进行处理,途中又需要占有文件 b。而另一方面,线程 2 占有着文件 b,途中又需要占有文件 a。大家不妨设想一下,如果线程 1 和线程 2 同时执行到了图中的箭头位置 会怎么样呢?线程 1 为了处理文件 b 想要获得锁 lockforb,但是它已经被线程 2 获得。同样,线程 2 想要获得的锁 lockfora 也被线程 1 占有着。这种情况下,线程 1 和线程 2 就会同时进入休眠状态, 而且双方都不能跳出该状态。

像这样,当多个线程互相等待资源的释放时,就非常容易出现死锁现象。有时是多个线程相干预,有时则是一个线程因为自己需要获得锁而进入休眠状态。此外,由于多数情况下各个线程本身 并没有错误处理,而且死锁又随时可能发生,因此追究原因就非常困难,也不能排除导致程序 bug 的可能。

尝试获得锁

nslock 类不仅能获得锁和释放锁,还有检查是否能获得锁的功能。利用这些功能,就可以在不 能获得锁时进行其他处理。

- (bool) trylock

用 接收器尝试获得某个锁,如果可以取得该锁则返回 yes。不能获得时,与lock 处理不同,线程没 有进入休眠状态,而是直接返回 no 并继续执行。

该方法十分便利,但要确保只能在可以获得锁时才执行 unlock,创建程序时必须注意这一点。

条件锁

类 nsconditionlock 称为 条件锁 (condition lock)。该锁持有整数值,根据该值可以获得锁或者 等待。

  • (id) initwithcondition: (nsinteger) condition

nsconditionlock 实例初始化,设置参数 condition 指定的值。

nscondtionlock 的指定初始化器。

  • (nsinteger) condition

此时返回锁中设定的值。

  • (void) lockwhencondition: (nsinteger) condition

如果锁正在被使用,则线程进入休眠状态。

锁不在被使用时,如果锁值和参数 condition 的值一致,则将锁状态修改为正在被使用,然后继续执 行,如果不一致,则线程进入休眠状态。

  • (void) unlockwithcondition: (nsinteger) condition

在锁中设置参数 condition 指定的值。将锁设置为不在被使用,此时如果有等待获得该锁且处于休眠 状态的线程,则将其唤醒。

  • (bool) trylockwhencondition: (nsinteger) condition

尚未使用锁且锁值与参数 condition 相同时,获得锁并返回 yes。不能获得锁时也不进入休眠状态, 而是返回 no,线程继续执行。

使用方法 lock 、 unlock 或 trylock 都可以获得锁和释放锁,而且无需关心锁的值。

然而,由于 nsconditionlock 实例可以持有的状态为整数型,所以事先用枚举常数或宏定义就可 以了。如果只使用 0 或 1,不仅不容易理解,也可能造成错误。

nsrecursivelock

某线程获得锁后,到该线程释放锁期间,想要获得该锁的线程就会进入休眠。使用类 nslock 的 锁时,如果已经获得锁的线程在没有释放它的情况下还想再次获得该锁,该线程也会进入休眠状态。 但是,由于没有从休眠状态唤醒的线程,所以这就是死锁。下面是一个简单的例子,这段代码不会 执行。

[alock lock];

[alock lock];      // 这里发生死锁

[alock unlock];

[alock unlock];

解决这种情况可以使用 nsrecursivelock 类的锁,拥有锁的线程即使多次获得同一个锁也不会 进入死锁。但是,其他线程当然也不能获得该锁。获得次数和释放次数一致时,锁就会被释放。

nsrecursivelock 类的锁使用起来十分方便,但排除被重复加锁的情况,用 nslock 来重新记述 
的话,性能则会更好。

@synchronized

程序内的块可以指定为不被多线程同时使用。为此可以使用 @synchronized 编译符,如下所示。

 
Objective-C编程 — 并行编程

通过使用该段代码,运行时系统就可以创建排斥地执行该代码块的锁(mutex)。参数 obj 通常指 定为该互斥锁要保护的对象。obj 自己不需要是锁对象。

线程如果要执行该代码块,首先会尝试获得锁,如果能获得锁则可以执行块内代码。块执行结 束时一并释放锁。使用 break 或 return 从块内跳出到块外时也被视作块执行终止。而且,在块内发生 异常时,运行时系统会捕捉异常并释放块。

@synchronized 的参数对象决定对应的块。所以, 同一个对象参数的 @synchronized 块如果有多 个,则不可以同时执行。

根据参数的选择方法的不同,@synchronized 会在并行执行的受限对象和可以执行的普通对象之 间动态切换。下面展示 @synchronized 参数的使用示例。

(a) 是指定只能单独存在的对象时的情景。同一个对象在其他地方也作为 @synchronized 的参数 使用时,所有这些块不能同时执行。(b) 也是一样,因为限制了参数的使用范围,互斥对象显然只能 是该方法内的块。

(c) 是各个实例互斥的例子。一个实例一次只能执行一个线程,同一类别的其他实例则多个线程可以同时存在。(d) 在参数对象可能在多个地方更改的情况下有效,但以同样方式使用该对象的所有 场所中都需要按照该方式书写,否则就没有任何意义。

而且,也可以按照 (e) 的方式书写。此外还可以指定类对象,或者使用消息选择器(隐藏参数的 _cmd)来指定方法等。不过一般情况下,为互斥的对象使用专门的锁对象是比较可靠的方法。

 
Objective-C编程 — 并行编程

使用 @synchronized 块时,加锁和解锁必须成对进行,因此可以防止加锁后忘记解锁这种问题的 发生。和普通的锁相比,复杂的并行算法的书写会较为复杂,但多数情况下都会使互斥更容易理解。
另外,如果你想一起进阶,不妨添加一下交流群1012951431,选择加入一起交流,一起学习。期待你的加入!