iOS多线程指南

本文将从以下几个部分来介绍多线程。

  • 第一部分介绍多线程的基本原理。
  • 第二部分介绍Run loop。
  • 第三部分介绍多线程的几种技术实现方式。
  • 第四部分介绍线程安全与同步。

第一部分:多线程的基本原理

基础术语:

  • 线程(线程)用于指代独立执行的代码段。
  • 进程(process)用于指代一个正在运行的可执行程序,它可以包含多个线程。
  • 任务(task)用于指代抽象的概念,表示需要执行工作。

多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径。多线程是一种技术,一种方法,这种方法使得单个应用程序能够执行多个代码块。从技术角度来看,一个线程就是一个需要管理执行代码的内核级和应用级数据结构组合。内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。 应用级结构包括用于存储函数调用的调用堆栈和应用程序需要管理和操作线程属性和状态的结构。

为了理解多线程种技术,我们先来了解一下线程的行为。

线程基本行为:

状态:

线程有三种状态:运行(running)、就绪(ready)、阻塞(blocked)。线程启动之后,线程就进入三个状态中的任何一个:运行(running)、就绪(ready)、阻塞(blocked)。如 果一个线程当前没有运行,那么它不是处于阻塞,就是等待外部输入,或者已经准备 就绪等待分配 CPU。线程持续在这三个状态之间切换,直到它最终退出或者进入中断状态。

分类:
线程分为可连接线程(Joinable thread )和脱离线程(Datached thread) 。

可连接线程类似于子线程。虽然你作为独立线程运行,但是可连接线 程在它资源可以被系统回收之前必须被其他线程连接。可连接线程同时提供了一个显 示的方式来把数据从一个正在退出的线程传递到其他线程。在它退出之前,可连接线 程可以传递一个数据指针或者其他返回值给 pthread_exit 函数。其他线程可以通过 pthread_join 函数来拿到这些数据。

脱离线程(Datached thread) 允许系统在线程完成的时候立 即释放它的数据结构。脱离线程同时不需要显示的和你的应用程序交互。

它们区别在于:在应用程序退出时,脱离线程可以立即被中断,而可连接线程则不可以。每个可连接 线程必须在进程被允许可以退出的时候被连接。

默认情况下只有应用程序的主线程是可连接的方式创建的,也就是说大部分上层的线程技术都默认创建了脱离线程(Datached thread)。当然在线程启动后, 你可以通过调用 pthread_detach 函数来把线程修改为可连接的。

优先级:

你创建的任何线程默认的优先级是和你本身线程相同。内核调度算法在决定该运 行那个线程时,把线程的优先级作为考量因素,较高优先级的线程会比较低优先级的 线程具有更多的运行机会。较高优先级不保证你的线程具体执行的时间,只是相比较 低优先级的线程,它更有可能被调度器选择执行而已。

配置堆栈:

对于每个你新创建的线程,系统会在你的进程空间里面分配一定的内存作为该线 程的堆栈。可以通过相关方法设置线程堆栈大小。

中断线程:

退出一个线程推荐的方法是让它在它主体入口点正常退出。经管 Cocoa、POSIX 和 Multiprocessing Services 提供了直接杀死线程的例程,但是使用这些例程是强 烈不鼓励的。杀死一个线程阻止了线程本身的清理工作。线程分配的内存可能造成泄 露,并且其他线程当前使用的资源可能没有被正确清理干净,之后造成潜在的问题。

如果你的应用程序需要在一个操作中间中断一个线程,你应该设计你的线程响应 取消或退出的消息。对于长时运行的操作,这意味着周期性停止工作来检查该消息是 否到来。如果该消息的确到来并要求线程退出,那么线程就有机会来执行任何清理和 退出工作;否则,它返回继续工作和处理下一个数据块。

第二部分:Run Loops:

Run loops 是线程相关的的基础框架的一部分。本质上一个 run loop 就是一个事件处理的循环,用于不停的调度工作以及处理输入事件。使用 run loop 的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

Cocoa 和 Core Fundation 都提供了 run loop objects 来帮助配置和管理你线程的 run loop。你的应用程序不需要显式的创建这些 对象(run loop objects);每个线程,包括程序的主线程都有与之对应的 run loop object。只有辅助线程(你创建的)需要显式的运行它的 run loop。在 Carbon 和 Cocoa 程序中, 主线程会自动创建并运行它 run loop,作为一般应用程序启动过程的一部分。

一个 run loop 是用来在线程上管理事件异步到达的基础设施。一个 run loop 为 线程监测一个或多个事件源。当事件到达的时候,系统唤醒线程并调度事件到 run loop,然后分配给指定程序。如果没有事件出现和准备处理,run loop 把线程置于休眠状态。runloop与线程息息相关,离开线程单独说runloop是没有意义的。

** runLoop 本质就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。**线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

基本概念:

实现对象

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()。第一次获取RunLoop时,将为你创建runloop对象。
  • NSRunLoop 是基于CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef


    A50FE6F8-E470-48D8-A1C3-5629FCDA7586.png

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

模式(Mode):

Run loop 模式(Mode)是所有要监视的输入源和定时源以及要通知的 run loop 注册观察者的集合。每次运行你的 run loop,你都要指定(无论显示还是隐式)其运行个模 式。在 run loop 运行过程中,只有和模式相关的源才会被监视并允许他们传递事件 消息。(类似的,只有和模式相关的观察者会通知 run loop 的进程)。和其他模式关联的源只有在 run loop 运行在其模式下才会运行,否则处于暂停状态。

输入源(Source):

输入源异步的发送消息给你的线程。 是事件产生的地方。事件来源取决于输入源的种类:基于端口的输入源和自定义输入源。

  • Source0(自定义输入源) 只包含了一个回调(函数指针,它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1(基于端口的输入源) 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。 Cocoa 和 Core Foundation 内置支持使用端口相关的对象和函数来创建的基于端 口的源。例如,在 Coco 里面你从来不需要直接创建输入源。你只要简单的创建端口 对象,并使用 NSPort 的方法把该端口添加到 run loop。端口对象会自己处理创建和 配置输入源。

备注:selector源本质上是Cocoa 定义了自定义输入源。它允许你在任何线程执行selector。并且是强制执行

当你创建输入源,你需要将其分配给 run loop 中的一个或多个模式。模式只会 在特定事件影响监听的源。

定时源(Timer)

定时源在预设的时间点同步方式传递消息。定时器是线程通知自己做某事的一种方法.它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

Run Loop 观察者(Observer)

每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
你可以使用 run loop 观察者来为处理某一特定事件或是进 入休眠的线程做准备。你可以将 run loop 观察者和以下事件关联:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry        = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};```

源是合适的同步或异步事件发生时触发,而 run loop 观察者则是在 run loop 本身运行的特定时候触发。

现在看看CFRunLoopMode 和 CFRunLoop 结构,你会对此清晰很多:

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};```

这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。

Run Loop 的事件队列

每次运行 run loop,你线程的 run loop 对会自动处理之前未处理的消息,并通 知相关的观察者。具体的顺序如下:


事件队列.png

因为定时器和输入源的观察者是在相应的事件发生之前传递消息,所以通知的时 间和实际事件发生的时间之间可能存在误差。如果需要精确时间控制,你可以使用休 眠和唤醒通知来帮助你校对实际发生事件的时间。

因为当你运行 run loop 时定时器和其它周期性事件经常需要被传递,撤销 run loop 也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到 消息而不是经由程序传递,因此活跃的定时器不会开始直到鼠标追踪结束并将控制权 交给程序。

Run loop 可以由 run loop 对象显式唤醒。其它消息也可以唤醒 run loop。例如, 添加新的非基于端口的源会唤醒 run loop 从而可以立即处理输入源而不需要等待其 他事件发生后再处理。

runloop实现原理:

介绍runloop原理之前先介绍一下ios系统层次

iOS的系统架构分为四个层次:核心操作系统层(Core OS layer)、核心服务层(Core Services layer)、媒体层(Media layer)和可触摸层(Cocoa Touch layer)。

  • 位于iOS系统架构最下面的一层是核心操作系统层(Core OS layer),它包括内存管理、文件系统、电源管理以及一些其他的操作系统任务。它可以直接和硬件设备进行交互。核心操作系统层包括以下这些组件:Accelerate Framework、External Accessory Framework、Security Framework、System等几个框架,基本都是基于c语言的接口。
  • 第二层是核心服务层,我们可以通过它来访问iOS的一些服务。包含:Address Book Framework、CFNetwork Framework、Core Data Framework、Core Foundation Framework、Core Location Framework、Core Media Framework、Core Telephony Framework、Event Kit Framework、Foundation Framework、Mobile Core Services Framework、Quick Look Framework、Store Kit Framework、System Configuration Framework、Block Objects、Grand Central Dispatch 、In App Purchase、Location Services、SQLite、XML Support等一些框架,也基本都是基于c语言的接口。
  • 第三层是媒体层,通过它我们可以在应用程序中使用各种媒体文件,进行音频与视频的录制,图形的绘制,以及制作基础的动画效果。它包括以下这些组件:
    Core Audio OpenGL Audio Mixing Audio Recording Video Playback JPG,PNG,TIFF PDF Quartz Core Animation OpenGL ES
  • 第四层是可触摸层,这一层为我们的应用程序开发提供了各种有用的框架,并且大部分与用户界面有关,本质上来说它负责用户在iOS设备上的触摸交互操作。它包括以下这些组件: Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls.

cocoa 很多组件都有两种实现,一种是基于 C 的以 CF 开头的类(CF=Core Foundation),这是比较底层的;另一种是基于 Obj-C 的以 NS 开头的类(NS=Next Step),这种类抽象层次更高,易于使用。

最上面一层也称:Darwin即操作系统的核心。


Darwin.png

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。 IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。

RunLoop 的核心就是一个 mach_msg() RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

runloop基本使用:
获得runloop对象

1.在 Cocoa 程序中,使用 NSRunLoop 的 currentRunLoop 类方法来检索一个 NSRunLoop 对象。
2.使用CFRunLoopGetCurrent函数。

2.配置 Run Loop

  • 在你在辅助线程运行 run loop 之前,你必须至少添加一输入源或定时器给它。
  • 你也可以添加run loop观察者来监视run loop的不同执行阶段情 况。为了给run loop添加一个观察者,你可以创建CFRunLoopObserverRef不透明类 型,并使用CFRunLoopAddObserver将它添加到你的run loop。
  • 当当前长时间运行的线程配置 run loop 的时候,最好添加至少一个输入源到 run loop 以接收消息。虽然你可以使用附属的定时器来进入 run loop,但是一旦定时器 触发后,它通常就变为无效了,这会导致 run loop 退出。虽然附加一个循环的定时 器可以让 run loop 运行一个相对较长的周期,但是这也会导致周期性的唤醒线程, 这实际上是轮询(polling)的另一种形式而已。与之相反,输入源会一直等待某事 件发生,在事情导致前它让线程处于休眠状态。

3.启动 Run Loop

启动 run loop 只对程序的辅助线程有意义。有几种方式可以启动 run loop,包括以下这些:

  • 无条件的
    无条件进入run loop是最简单的方法,但是它会使你的线程处于一个永久的循环之中。
  • 设置超时时间
    设置超时时间进入runloop 后。runloop会运作到某一事件到达或者规定时间已经到期。
  • 特定的模式
    模式和超时不是 互斥的,他们可以在启动 run loop 的时候同时使用。模式限制了可以传递事件给 run loop 的输入源的类型

4.退出Run Loop

  • 1.给 run loop 设置超时时间
  • 2.通知 run loop 停止

使用 CFRunLoopStop 来显式的停止 run loop 和使用超时时间产生的结果相似。 Run loop 把所有剩余的通知发送出去再退出。与设置超时的不同的是你可以在无条 件启动的 run loop 里面使用该技术。

尽管移除 run loop 的输入源和定时器也可能导致 run loop 退出,但这并不是可 靠的退出 run loop 的方法。一些系统例程会添加输入源到 run loop 里面来处理所需 事件。因为你的代码未必会考虑到这些输入源,这样可能导致你无法没从系统例程中 移除它们,从而导致退出 run loop。

配置runloop的源:

待续

第三部分介绍多线程的几种实现方式。

iOS支持三个层次的线程编程。分别是:

  • Thread
  • Cocoa Operations
  • Grand Central Dispatch技术

Thread技术:
Thread包含两种:

  • 1.Cocoa threads
    优点:NSThread 比其他两个轻量级
    缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
    创建:
-(id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
+(void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];
NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                     selector:@selector(doSomething:)
                                         object:nil];
 [myThread start];

用NSObject的类方法 performSelectorInBackground:withObject:
通信:

[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES]
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
[self performSelector:@selector(run) withObject:nil];

相关的一些方法:

//取消线程
-(void)cancel;
//启动线程
-(void)start;
//判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
//设置和获取线程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
//获取当前线程信息
+(NSThread *)currentThread;
//获取主线程信息
+(NSThread *)mainThread;
//使当前线程暂停一段时间,或者暂停到某个时刻
+(void)sleepForTimeInterval:(NSTimeInterval)time;
+(void)sleepUntilDate:(NSDate *)date;
  • 2.POSIX threads: 基于 C 语言的一个多线程库。

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    pthread_t thread;
    //创建一个线程并自动执行
    pthread_create(&thread, NULL, start, NULL);
}
void *start(void *data) {
    NSLog(@"%@", [NSThread currentThread]);

    returnNULL;
}
Grand Central Dispatch#####

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。

优势:
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

dispatch object并不参与垃圾回收系统,所以即使开启了GC,你也必须手动管理GCD对象的内存。

基本概念:

队列(dispatch queue):dispatch queue是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。

  • 串行队列(SerialDispatchQueue):同一时间只执行单一任务。
  • 并发队列(ConcurrentDispatchQueue):可以让多个任务并发执行,并发功能只有在一异步函数下才有效果。
  • 同步:在当前线程中执行
  • 异步:在另一条线程中执行

GCD中的队列:

  • 1.MainDispatchQueue:

与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。一般用来执行UI方面的操作。

  • 2.Global queues

全局队列是并发队列,并由整个进程共享。进程中存在三个全局队列:高、中(默认)、低、后台四个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队列。
GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。使用dispatch_get_global_queue函数获得全局的并发队列
备注:系统提供了两个队列,一个是MainDispatchQueue,一个是GlobalDispatchQueue。

  • 3.用户队列:

用函数 dispatch_queue_create 创建的队列. 这些队列是串行的。正因为如此,它们可以用来完成同步机制。

几个重要方法:

dispatch_once  它可以保证整个应用程序生命周期中某段代码只被执行一次!
dispatch_after   几秒钟后执行
dispatch_set_target_queue 设置一个dispatch queue的优先级
dispatch_apply  执行某个代码片段若干次。
dispatch group Dispatch Group机制允许我们监听一组任务是否完成
 dispatch_barrier_async 通过dispatch_barrier_async函数提交的任务会等它前面的任务执行结束才开始,然后它后面的任务必须等它执行完毕才能开始
Cocoa Operations#####

Cocoa Operations多线程技术主要有两个类:NSOperationQueue与NSOperation。
NSOperation

一个operation就相当于一个代码块,这里只是把它提高到一种任务的角度来看待,然后,任务便会有开始执行(start)、取消(cancel)、是否取消(isCancel)、是否完成(isFinishing)、暂停(pause)等状态函数,其本身是不会创建新的线程来执行它的,NSOperation本身是抽象基类,因此必须使用它的子类,使用NSOperation子类的方式有2种:1

  • Foundation框架提供了两个具体子类直接供我们使用:NSInvocationOperation和NSBlockOperation
    程安全与同步
  • 自定义子类继承NSOperation,实现内部相应的方法。

NSOperationQueue

OperationQueue实质上也就是数组管理,对添加进去的operation进行管理、创建线程等;添加到queue里的operation,queue默认会调用operation的start函数来执行任务,而start函数默认又是调用main函数的。默认是同步执行的。

如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作

添加操作到NSOperationQueue中,自动执行操作,自动开启线程

使用步骤

NSOperation和NSOperationQueue实现多线程的具体步骤:
(1)先将需要执行的操作封装到一个NSOperation对象中
(2)然后将NSOperation对象添加到NSOperationQueue中
(3)系统会⾃动将NSOperationQueue中的NSOperation取出来
(4)将取出的NSOperation封装的操作放到⼀条新线程中执⾏

第四部分 线程安全与同步

两个线程同时修改同一资源有可能以意想不到的方式互相干扰。使我的程序发生问题,iOS提 􏰁供了你可以使用的多个同步工具,从􏰁供互斥访问你程序的有序的事件的工具等来解决线程间同步的问题。
1.原子操作:

原子操作是同步的一个简单的形式,它处理简单的数据类型。原子操作的优势是它们不妨碍竞争的线程。对于简单的操作,比如递增一个计数器,原子操作比使用锁具有更高的性能优势。

2.条件信号量

条件是信号量的另外一个形式,它允许在条件为真的时候线程间互相发送信号。条件通常被使用来说明资源可用性,或用来确保任务以特定的顺序执行。当一个线程测试一个条件时,它会被阻塞直到条件为真。它会一直阻塞直到其他线程显
信号量的状态。条件和互斥锁(mutex lock)的区别在于多个线程被允许同时访问一个条件。条件更多是允许不同线程根据一些指定的标准通过的守门人。

3.锁

锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section),这些代码段在同一个时间只能允许被一个线程访问。比如,一个临界区可能会操作一个特定的数据结构,或使用了每次只能一个客户端访问的资源。


  • 使用POSIX互斥锁

POSIX 互斥锁在很多程序里面很容易使用。为了新建一个互斥锁,你声明并初始化一个 pthread_mutex_t 的结构。为了锁住和解锁一个互斥锁,你可以使用pthread_mutex_lock 和 pthread_mutex_unlock 函数。列表 4-2 显式了要初始化并使用一个 POSIX 线程的互斥锁的基础代码。当你用完一个锁之后,只要简单的调用pthread_mutex_destroy 来释放该锁的数据结构。

pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
  • NSLock锁

在 Cocoa 程序中 NSLock 中实现了一个简单的互斥锁。所有锁(包括 NSLock)的接口实际上都是通过 NSLocking 协议定义的,它定义了 lock 和 unlock 方法。你使用这些方法来获取和释放该锁。

除了标准的锁行为,NSLock 类还增加了 tryLock 和 lockBeforeDate:方法。方法tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程。相反,它只是返回 NO。而 lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回 NO)。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
  • 使用@synchronized指令

@synchronized 指令是在 Objective-C 代码中创建一个互斥锁非常方便的方法。@synchronized 指令做和其他互斥锁一样的工作(它防止不同的线程在同一时间获取同一个锁)。然而在这种情况下,你不需要直接创建一个互斥锁或锁对象。相反,你只需要简单的使用 Objective-C 对象作为锁的令牌,如下面例子所示:

-(void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}

作为一种预防措施,@synchronized 块隐式的添加一个异常处理例程来保护代码。该处理例程会在异常抛出的时候自动的释放互斥锁。这意味着为了使用@synchronized 指令,你必须在你的代码中启用异常处理。了如果你不想让隐式的异常处理例程带来额外的开销,你应该考虑使用锁的类。

  • 其他锁
    NSRecursiveLock 递归锁

NSRecursiveLock 类定义的锁可以在同一线程多次获得,而不会造成死锁。一个递归锁会跟踪它被多少次成功获得了。每次成功的获得该锁都必须平衡调用锁住和解锁的操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。
正如它名字所言,这种类型的锁通常被用在一个递归函数里面来防止递归造成阻塞线程。你可以类似的在非递归的情况下使用他来调用函数,这些函数的语义要求它们使用锁。以下是一个简单递归函数,它在递归中获取锁。如果你不在该代码里使用NSRecursiveLock 对象,当函数被再次调用的时候线程将会出现死锁。
NSConditionLock
NSConditionLock 对象定义了一个互斥锁,可以使用特定值来锁住和解锁。不要把该类型的锁和条件混淆了。它的行为和条件有点类似,但是它们的实现非常不同。
通常,当多线程需要以特定的顺序来执行任务的时候,你可以使用一个NSConditionLock 对象,比如当一个线程生产数据,而另外一个线程消费数据。生产者执行时,消费者使用由你程序指定的条件来获取锁(条件本身是一个你定义的整形值)。当生产者完成时,它会解锁该锁并设置锁的条件为合适的整形值来唤醒消费者线程,之后消费线程继续处理数据。

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
while(true)
{
[condLock lock];
[condLock unlockWithCondition:HAS_DATA];
}

4. 执行Selector例程
Cocoa 程序包含了一个在一个线程以同步的方式传递消息的方便方法。NSObject类声明方法来在应用的一个活动线程上面执行 selector 的方法。这些方法允许你的线程以异步的方式来传递消息,以确保它们在同一个线程上面执行是同步的。比如,你可以通过执行 selector 消息来把一个从你分布计算的结果传递给你的应用的主线程或其他目标线程。每个执行 selector 的请求都会被放入一个目标线程的 run loop的队列里面,然后请求会按照它们到达的顺序被目标线程有序的处理。
5.内存屏障和 Volatile 变量

内存屏障(memory barrier)是一个使用来确保内存操作按照正确的顺序工作的非阻塞的同步工具。内存屏障的作用就像一个栅栏,迫使处理器来完成位于障碍前面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。内存屏障同样使用来确保一个线程(但对另外一个线程可见)的内存操作总是按照预定的顺序完成。如果在这些地方缺少内存屏障有可能让其他线程看到看似不可能的结果(比如,内存屏障的维基百科条目)。为了使用一个内存屏障,你只要在你代码里面需要的地方简单的调用 OSMemoryBarrier 函数。

Volatile 变量适用于独立变量的另一个内存限制类型。编译器优化代码通过加载这些变量的值进入寄存器。对于本地变量,这通常不会有什么问题。但是如果一个变量对另外一个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化。在变量之前加上关键字 volatile 可以强制编译器每次使用变量的时候都从内存里面加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该变量声明为 volatile 变量。

因为内存屏障和 volatile 变量降低了编译器可执行的优化,因此你应该谨慎使用它们,只在有需要的地方时候,以确保正确性。关于更多使用内存屏障的信息,参阅 OSMemoryBarrier 主页。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容