本文首发CSDN,如需转载请与CSDN联系。

本文首发CSDN,如需转载请与CSDN联系。

本文首发CSDN,如需转载请与CSDN联系。

本文首发CSDN,如需转载请与CSDN联系。

View and Window Architecture

记得第一次读这个文档还是3年前,那时也只是泛读。如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objc与Swift转换的参考。官方文档地址:Threading
Programming Guide

记得第一次读这个文档还是3年前,那时也只是泛读。如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objc与Swift转换的参考。官方文档地址:Threading
Programming Guide

记得第一次读这个文档还是3年前,那时也只是泛读。如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objc与Swift转换的参考。官方文档地址:Threading
Programming Guide

记得第一次读这个文档还是3年前,那时也只是泛读。如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objc与Swift转换的参考。官方文档地址:Threading
Programming Guide

View Hierarchies and Subview Management

  • 父视图维护着一个子视图数组,数组最后一个元素就是父视图内顶部的视图

线程也是具有若干属性的,自然一些属性也是可配置的,在启动线程之前我们可以对其进行配置,比如线程占用的内存空间大小、线程持久层中的数据、设置线程类型、优先级等。

配置Timer事件源拢共分几步?很简单,大体只有两步,先创建Timer对象,然后将其添加至Run
Loop中。在Cocoa框架和Core
Foundation框架中都提供了相关的对象和接口,在Cocoa框架中,它为我们提供了NSTimer类,该类有两个类方法,可以让我们很方便的在当前线程的Run
Loop中配置Timer事件源:

我们设想在应用程序中,每行代码的执行都有一个执行路径并对应一个执行容器。线程,可以让应用程序中的代码通过多个执行路径执行,从而达到多个代码块同时在不同的执行路径下执行运算,即多任务同时执行。

前文中多次提到过,在主线程中Run
Loop是随着应用程序一起启动的,也就是说当我们打开一个应用时,主线程中的Run
Loop就已经启动了,尤其现在我们都使用Xcode中的项目模版创建项目,更是不用考虑主线程中Run
Loop的状体。所以只有在二级线程中,也就是我们自己创建的线程中才有机会手动的创建的Run
Loop,并对其进行配置的操作。

The View Drawing Cycle

  • 不改变视图内容不会调用重绘代码
  • 改变视图内容不会马上进行重绘,需要调用 setNeddsDisplay 或者
    setNeddsDisplayInRect ,这两个方法会告诉系统需要重绘,系统会在当前
    runloop 结束以后开始重绘,这段时间还可以进行更改
  • 改变视图的形状也不会调用重绘代码,只有当设置 contentMode 属性为
    UIViewContentModeRedraw 时才会调用

配置线程的栈空间大小

在前文中提到过线程对内存空间的消耗,其中一部分就是线程栈,我们可以对线程栈的大小进行配置:

  • Cocoa框架:在OS X
    v10.5之后的版本和iOS2.0之后的版本中,我们可以通过修改NSThread类的stackSize属性,改变二级线程的线程栈大小,不过这里要注意的是该属性的单位是字节,并且设置的大小必须得是4KB的倍数。
  • POSIX
    API:通过pthread_attr_- setstacksize函数给线程属性pthread_attr_t结构体设置线程栈大小,然后在使用pthread_create函数创建线程时将线程属性传入即可。

注意:在使用Cocoa框架的前提下修改线程栈时,不能使用NSThreaddetachNewThreadSelector: toTarget:withObject:方法,因为上文中说过,该方法先创建线程,即刻便启动了线程,所以根本没有机会修改线程属性。

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats::该方法有五个参数分别是执行事件消息时间间隔、接收事件消息的目标对象、事件消息、发送给事件消息的参数、是否重复执行标识。

在系统中,每个程序都是并行状态的,但是并不是一直持续着活跃状态,而是由系统根据程序的需要适时的分配执行时间和内存。在每个程序中,或许存在多个线程,执行着不同的任务,那么系统对程序执行的管理实际上就是对程序中线程的管理,比如适时的将某个线程安排到负载较小的内核中执行,或者阻止正在运行的优先级较低的线程,给优先级较高的线程让路等。所以说线程的运转需要内核级别和应用程序级别相互协调,即内核级别负责将事件分发给不同的线程,并将线程安排在合理的内核上执行以及管理线程的优先级,而应用程序级别是通过代码管理和操控线程的属性及状态。

在前文中还提到过,Run
Loop在线程中的主要作用就是帮助线程常驻在进程中,并且不会过多消耗资源。所以说Run
Loop在二级线程中也不是必须需要的,要根据该线程执行的任务类型以及在整个应用中担任何作用而决定是否需要使用Run
Loop。比如说,如果你创建一个二级线程只是为了执行一个不会频繁执行的一次性任务,或者需要执行很长时间的任务,那么可能就不需要使用Run
Loop了。如果你需要一个线程执行周期性的定时任务,或者需要较为频繁的与主线程之间进行交互,那么就需要使用Run
Loop。归纳一下需要使用Run Loop的情况大概有以下四点:

View Geometry and Coordinate Systems

  • iOS坐标系原点是左上角,向右是 X 轴,向下是 Y 轴,UIWindows 和 UIView
    有方法可以将一个坐标系转换到另一个坐标系

配置线程存储字典

每一个线程,在整个生命周期里都会有一个字典,以key-value的形式存储着在线程执行过程中你希望保存下来的各种类型的数据,比如一个常驻线程的运行状态,线程可以在任何时候访问该字典里的数据。

在Cocoa框架中,可以通过NSThread类的threadDictionary属性,获取到NSMutableDictionary类型对象,然后自定义key值,存入任何里先储存的对象或数据。如果使用POSIX线程,可以使用pthread_setspecificpthread_getspecific函数设置获取线程字典。

为什么要使用线程

回到iOS,我们开发的App至少都有一个线程,称之为主线程,线程中执行方法或函数的原则是先进先出原则,一个接一个的执行。假设在我们的App中有从远程下载图片的功能,并且该功能放在主线程中执行,那么当下载一个1080p高清图片时,就会需要耗费较长的时间,如果主线程中下载功能后面还有其他待执行的方法,那么只能等待下载功能完成之后,才能继续执行。所以此时对于用户来说,得不到任何来自App的响应,那么很容易认为是你的App出问题了,如此糟糕的用户体验,足以让用户将你的App打入冷宫甚至删除。

如果我们使用另外一个线程专门处理下载功能,那么该线程和主线程同时执行,对于用户而言,此时可以由主线程对用户做出合适的响应,而下载在另一个线程中同时进行着。所以使用线程对提高程序的用户体验、性能无疑是最好的方法。

  • 通过基于端口或自定义的数据源与其他线程进行交互。
  • 在线程中执行定时事件源的任务。
  • 使用Cocoa框架提供的performSelector…系列方法。
  • 在线程中执行较为频繁的,具有周期性的任务。

The Relationship of the Frame, Bounds, and Center Properties

  • frame 指的是在父视图坐标系中指定了位置和大小,设置该值影响 bounds 和
    center
  • bounds 指的是在自己的坐标系中指定了大小,设置该值影响 frame
  • center
    指的是在父视图坐标系中指定一个点作为自己的中心点,设置该值影响frame
  • 子视图超出父视图的区域可以使用 clipsTobounds 消除

配置线程类型

在上文中提到过,线程有Joinable和Detached类型,大多数非底层的线程默认都是Detached类型的,相比Joinable类型的线程来说,Detached类型的线程不用与其他线程结合,并且在执行完任务后可自动被系统回收资源,而且主线程不会因此而阻塞,这着实要方便许多。

使用NSThread创建的线程默认都是Detached类型,而且似乎也不能将其设置为Joinable类型。而使用POSIX
API创建的线程则默认为Joinable类型,而且这也是唯一创建Joinable类型线程的方式。通过POSIX
API可以在创建线程前通过函数pthread_attr_setdetachstate更新线程属性,将其设置为不同的类型,如果线程已经创建,那么可以使用pthread_detach函数改变其类型。Joinable类型的线程还有一个特性,那就是在终止之前可以将数据传给与之相结合的线程,从而达到线程之间的交互。即将要终止的线程可以通过pthread_exit函数传递指针或者任务执行的结果,然后与之结合的线程可以通过pthread_join函数接受数据。

虽然通过POSIX
API创建的线程使用和管理起来较为复杂和麻烦,但这也说明这种方式更为灵活,更能满足不同的使用场景和需求。比如当执行一些关键的任务,不能被打断的任务,像执行I/O操作之类。

NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "fireTimer:", userInfo: "This is a arg", repeats: true)func fireTimer(sender: NSTimer) { print("Fire timer...\(sender.userInfo as! String)") }

使用线程会导致的问题

俗话说天下没有免费的午餐,诚然多线程能提高程序的性能、用户体验,但是在光鲜的背后还是要承担一定风险的。使用多线程势必会增加开发人员写代码花费的时间,因为代码的复杂度变高了,开发人员斟酌的频率就会变高,线程与线程之间有交互,容错率就会降低,开发人员调试的时间就会变多。由于多线程依然共享内存,所以会发生两个线程同时对某个数据进行操作,这样很容易使程序的执行结果发生错误。总而言之,多线程好,但使用时要知其根本,做到佩弦自急。

因为线程本身相对比较低层,它实现程序中并发执行任务功能的方式也较为复杂,所以我们如果想使用好线程,那么就必须要真正理解线程,要明白在我们的程序中使用线程之后会带来哪些潜在的风险,所谓知己知彼方能百战不殆。同时,我们也不能滥用线程,该用的时候用,不该用的时候就不要画蛇添足。毕竟,使用线程会增加内存的消耗以及CPU得运算时间,要避免物极必反。在真正理解线程之前,我们先看看在OS
X和iOS中提供的不那么底层的实现多任务并发执行的解决方案:

  • Operation object:该技术出现在OS X
    10.5中,通过将要执行的任务封装成操作对象的方式实现任务在多线程中执行。任务可以理解为你要想执行的一段代码。在这个操作对象中不光包含要执行的任务,还包含线程管理的内容,使用时通常与操作队列对象联合使用,操作队列对象会管理操作对象如何使用线程,所以我们只需要关心要执行的任务本身即可。

  • GCD:该技术出现在OS X 10.6中,它与Operation
    Object的初衷类似,就是让开发者只关注要执行的任务本身,而不需要去关注线程的管理。你只需要创建好任务,然后将任务添加到一个工作队列里即可,该工作队列会根据当前CPU性能及内核的负载情况,将任务安排到合适的线程中去执行。

  • Idle-time
    notification:该技术主要用于处理优先级相对比较低、执行时间比较短的任务,让应用程序在空闲的时候执行这类任务。Cocoa框架提供NSNotificationQueue对象处理空闲时间通知,通过使用NSPostWhenIdle选项,向队列发送空闲时间通知的请求。

  • Asynchronous
    functions:系统中有一些支持异步的函数,可以自动让你的代码并行执行。这些异步函数可能通过应用程序的守护进程或者自定义的线程执行你的代码,与主进程或主线程分离,达到并行执行任务的功能。

  • Timers:我们也可以在应用程序主线程中使用定时器去执行一些比较轻量级的、有一定周期性的任务。

  • Separate
    processes:虽然通过另起一个进程比线程更加重量级,但是在某些情况下要比使用线程更好一些,比如你需要的执行的任务和你的应用程序在展现数据和使用方面没有什么关系,但是可以优化你的应用程序的运行环境,或者提高应用程序获取数据的效率等。

光说不练假把式,下面就让我们来看看如何具体创建、配置、操作Run Loop。

Tips for Using Views Effectively

  • 尽量减少绘制
  • 不设置 contentMode 属性为 UIViewContentModeRedraw
  • 设置视图为不透明
  • 滚动会在短时间内产生大量的视图更新,建议在滚动时可以改变 contentMode
    属性或者暂时改变呈现内容的质量
  • 不在视图中嵌入自定义视图,比如按钮中嵌入一个自定义图片

设置线程优先级

每一个新创建的二级线程都有它自己的默认优先级,内核会根据线程的各属性通过分配算法计算出线程的优先级。这里需要明确一个概念,高优先级的线程虽然会更早的运行,但这其中并没有执行时间效率的因素,也就是说高优先级的线程会更早的执行它的任务,但在执行任务的时间长短方面并没有特别之处。

不论是通过NSThread创建线程还是通过POSIX
API创建线程,他们都提供了设置线程优先级的方法。我们可以通过NSThread的类方法setThreadPriority:设置优先级,因为线程的优先级由0.0~1.0表示,所以设置优先级时也一样。我们也可以通过pthread_setschedparam函数设置线程优先级。

注意:设置线程的优先级时可以在线程运行时设置。

虽然我们可以调节线程的优先级,但不到必要时还是不建议调节线程的优先级。因为一旦调高了某个线程的优先级,与低优先级线程的优先等级差距太大,就有可能导致低优先级线程永远得不到运行的机会,从而产生性能瓶颈。比如说有两个线程A和B,起初优先级相差无几,那么在执行任务的时候都会相继无序的运行,如果将线程A的优先级调高,并且当线程A不会因为执行的任务而阻塞时,线程B就可能一直不能运行,此时如果线程A中执行的任务需要与线程B中任务进行数据交互,而迟迟得不到线程B中的结果,此时线程A就会被阻塞,那么程序的性能自然就会产生瓶颈。

在任何平台,线程存在的价值和意义都是一样的,那就是执行任务,不论是方法、函数或一段代码,除了依照语言语法正常编写外,还有一些额外需要大家注意的事项。

  • scheduledTimerWithTimeInterval:invocation:repeats::该方法有三个参数,分别是执行事件消息事件间隔、NSInvocation对象、是否重复执行标识。这里说一下NSInvocation类,该类的作用是静态渲染消息,说的简单粗暴一点,那就是该类表示某个对象中的某个方法,以及该方法的一个或多个参数和返回值,当我们需要发送有多个参数或者有返回值的消息时就可以用这个类。但是在Swift中无法使用这个类,这里就不做过多说明了。

线程技术

说到OS X和iOS中的线程技术,就不得不说GNU
Mach。Apple操作系统中的线程技术是基于Mach线程技术实现的,所以本身就带有线程基本的特性,比如PEM。Mach线程我们几乎不会用到,一般编程中我们可能会使用POSIX
API创建线程。

GNU Mach:GNU是一个类UNIX操作系统,它采用GNU
Hurd作为操作系统内核,而GNU Mach是基于GNU
Hurd内核技术的微内核。POSIX:可移植操作系统接口(Portable Operating
System Interface of
UNIX),它定义了操作系统应该为应用程序提供的接口标准,
是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称。PEM:Preemptive
Execution
Model,以任务的优先级决定立即执行还是延后执行,或者安排至不同的内核执行。

我们来看看OS X和iOS中主要的两种线程技术:

  • Cocoa
    Threads:Cocoa框架中提供了NSThreadNSObject类供我们进行线程相关的操作。
  • POSIX
    Threads:POSIX的线程API实际是基于C语言的线程接口,这些接口在使用线程和配置线程方面更加容易和灵活。

在应用程序层面,不管是什么平台,线程的运行方式都是大体相同的,在线程的运行过程中一般都会经历三种状态,即运行中、准备运行、阻塞。如果某个线程在当前处于不活跃状态,也即是非运行中状态,那么它有可能是处于阻塞状态并在等待执行任务的输入。也有可能已经有任务输入,处于准备运行状态,只是在等待被分派。当我们终止线程后,它会永久性的被系统回收,因为毕竟线程会占用一定的系统内存和CPU运算时间,所以一般情况下,我们放入二级线程中的任务都是比较重要和有意义的任务。

要想操作配置Run Loop,那自然需要通过Run
Loop对象来完成,它提供了一系列接口,可帮助我们便捷的添加Input
sources、timers以及观察者。较高级别的Cocoa框架提供了NSRunLoop类,较底层级别的Core
Foundation框架提供了指向CFRunloopRef的指针。

Views

Autorelease Pool

在Xcode4.3之前,我们都处在手动管理引用计数的时代,代码里满是retainrelease的方法,所以那个时候,被线程执行的任务中,为了能自动处理大量对象的retainrelease操作,都会使用NSAutoreleasePool类创建自动释放池,它的作用是将线程中要执行的任务都放在自动释放池中,自动释放池会捕获所有任务中的对象,在任务结束或线程关闭之时自动释放这些对象:

- myThreadMainRoutine{ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 顶层自动释放池 // 线程执行任务的逻辑代码 [pool release];}

到了自动引用计数时代,就不能使用NSAutoreleasePool进行自动释放池管理了,而是新加了@autoreleasepool代码块语法来创建自动释放池:

- myThreadMainRoutine{ @autoreleasepool { // 线程执行任务的逻辑代码 }}

我们知道每个应用程序都是运行在一个主线程里的,而线程都至少得有一个自动释放池,所以说整个应用其实是跑在一个自动释放池中的。大家都知道C系语言中,程序的入口函数都是main函数,当我们创建一个Objective-C的iOS应用后,Xcode会在Supporting
Files
目录下自动为我们创建一个main.m文件:

图片 1LearnThread-2

main.m这个文件中就能证实上面说的那点:

int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}

以上都是在Objective-C中,但在Swift中,就有点不一样了,NSAutoreleasePool@autoreleasepool都不能用了,取而代之的是Swift提供的一个方法func autoreleasepool ->,接收的参数为一个闭包,我们可以这样使用:

func performInBackground() { autoreleasepool({ // 线程执行任务的逻辑代码 print("I am a event, perform in Background Thread.") }) }

根据尾随闭包的写法,还可以这样使用:

func performInBackground() { autoreleasepool{ // 线程执行任务的逻辑代码 print("I am a event, perform in Background Thread.") } }

有些人可能会问在ARC的时代下为什么还要用自动释放池呢?比如在SDWebImage中就大量使用了@autoreleasepool代码块,其原因就是为了避免内存峰值,大家都知道在MRC时代,除了retainrelease方法外,还有一个常用的方法是autorelease,用来延迟释放对象,它释放对象的时机是当前runloop结束时。到了ARC时代,虽然不用我们手动管理内存了,但其自动管理的本质与MRC时是一样的,只不过由编译器帮我们在合适的地方加上了这三个方法,所以说如果在一个线程执行的任务中大量产生需要autorelease的对象时,因为不能及时释放对象,所以就很有可能产生内存峰值。那么在这种任务中在特定的时候使用@autorelease代码块,帮助释放对象,就可以有效的防止内存峰值的发生。

以上两个类方法所添加的Timer事件源都只能添加在当前线程的Run
Loop中,并且是在默认的Run
Loop模式下(NSDefaultRunLoopMode),如果我们想将Timer事件源添加至其他线程Run
Loop的其他模式下,那么就需要创建NSTimer对象,并使用NSRunLoopaddTimer:forMode:方法添加创建好的NSTimer对象:

RunLoops

上一节提到当线程终止后就会永久被系统收回,如果你还有任务需要另起线程执行,就要重新创建线程以及配置,但这也不是必须的,我们可以让线程在空闲的时候休眠,当有任务需要执行时唤醒,就像主线程一样,此时就要用到RunLoop。

简单的来说,RunLoop用于管理和监听异步添加到线程中的事件,当有事件输入时,系统唤醒线程并将事件分派给RunLoop,当没有需要处理的事件时,RunLoop会让线程进入休眠状态。这样就能让线程常驻在进程中,而不会过多的消耗系统资源,达到有事做事,没事睡觉的效果。

主线程中的RunLoop系统已经自动帮我们配置好了,但是我们自己创建的线程,还需要对RunLoop配置一番才可以使用,在后面的章节中都会有详细介绍。

获取Run Loop对象

前文中提到过,在Cocoa和Core Foundation框架中都没有提供创建Run
Loop的方法,只有从当前线程获取Run Loop的方法:

  • 在Cocoa框架中,NSRunLoop类提供了类方法currentRunLoop()获取NSRunLoop对象。

    该方法是获取当前线程中已存在的Run
    Loop,如果不存在,那其实还是会创建一个Run
    Loop对象返回,只是Cocoa框架没有向我们暴露该接口。

  • 在Core
    Foundation框架中提供了CFRunLoopGetCurrent()函数获取CFRunLoop对象。

虽然这两个Run
Loop对象并不完全等价,它们之间还是可以转换的,我们可以通过NSRunLoop对象提供的getCFRunLoop()方法获取CFRunLoop对象。因为NSRunLoopCFRunLoop指向的都是当前线程中同一个Run
Loop,所以在使用时它们可以混用,比如说要给Run
Loop添加观察者时就必须得用CFRunLoop了。

Tweaking the Layout of Your Views Manually

  • 当 view 的 size 改变时,会调用 layoutSubviews 方法
  • 在滚动时会频繁 layoutsubview,当滚动事件开始,跟视图调用
    setNeedsLayoutlayoutSubviews 方法会根据偏移量来更改视图位置
  • 在写布局代码时,注意以下两点
    • 改变设备方向时
    • 状态栏高度改变,例如打电话时状态栏高度会增加

设置异常处理

在线程执行任务的时候,难免会出现异常,如果不能及时捕获异常任由其抛出,就会导致整个应用程序退出。在Swift2.0中,Apple提供了新的异常控制处理机制,让我们能像Java中一样形如流水的捕获处理异常。所以在线程执行的任务中,我们尽量使用异常处理机制,提高健壮性。

import Foundationclass CustomThread: NSThread { var myTimer: NSTimer! init(myTimer: NSTimer) { self.myTimer = myTimer } override func main() { autoreleasepool{ let runloop = NSRunLoop.currentRunLoop() runloop.addTimer(self.myTimer, forMode: NSRunLoopCommonModes) print(NSThread.isMultiThreaded runloop.runUntilDate(NSDate(timeIntervalSinceNow: 5)) } } }class TestThread: NSObject { func testTimerSource() { let fireTimer = NSDate(timeIntervalSinceNow: 1) let myTimer = NSTimer(fireDate: fireTimer, interval: 0.5, target: self, selector: "timerTask", userInfo: nil, repeats: true) let customThread = CustomThread(myTimer: myTimer) customThread.start() sleep } func timerTask() { print("Fire timer...") }}let testThread = TestThread()testThread.testTimerSource()

同步策略

诚然,使用线程好处多多,但是之前也提到过,使用线程也是会存在一定问题的,那就是资源竞争,当两个线程在同一时间操作同一个变量时,就会产生问题。一种解决方案是让不同的线程拥有各自独有的变量,虽然可以解决问题,但不是最优方案。较为优雅一些的方案则是使用线程中的同步策略来解决该问题。

常用的同步策略有线程锁、状态位、原子操作。线程锁较为简单粗暴,简单的说当一个线程在操作变量时会挂上一把互斥锁,如果另一个线程先要操作该变量,它就得获得这把锁,但是锁只有一个,必须等第一个线程释放互斥锁后,才可以被其他线程获取,所以这样就解决了资源竞争的问题。状态位策略是通过线程或任务的执行情况生成一个状态,这个状态即像门卫又像协管员,一是阻止线程进行,二是以合适的执行顺序安排协调各个任务。第三个策略则是原子操作,相对前两个策略要更轻量级一些,它能通过硬件指令保证变量在更新完成之后才能被其他线程访问。

配置Run Loop观察者

前文中提到过,可以向Run
Loop中添加各种事件源和观察者,这里事件源是必填项,也就是说Run
Loop中至少要有一种事件源,不论是Input source还是timer,如果Run
Loop中没有事件源的话,那么在启动Run
Loop后就会立即退出。而观察者是可选项,如果没有监控Run
Loop各运行状态的需求,可以不配置观察者,这一节先看看如何向Run
Loop中添加观察者。

在Cocoa框架中,并没有提供创建配置Run
Loop观察者的相关接口,所以我们只能通过Core
Foundation框架中提供的对象和方法创建并配置Run
Loop观察者,下面我们看看示例代码:

import Foundationclass TestThread: NSObject { func launch() { print("First event in Main Thread.") NSThread.detachNewThreadSelector("createAndConfigObserverInSecondaryThread", toTarget: self, withObject: nil) print(NSThread.isMultiThreaded sleep print("Second event in Main Thread.") } func createAndConfigObserverInSecondaryThread() { autoreleasepool{ // 1 let runloop = NSRunLoop.currentRunLoop() // 2 var _self = self // 3 var observerContext = CFRunLoopObserverContext(version: 0, info: &_self, retain: nil, release: nil, copyDescription: nil) // 4 let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.AllActivities.rawValue, true, 0, self.observerCallbackFunc(), &observerContext) if(observer != nil) { // 5 let cfRunloop = runloop.getCFRunLoop() // 6 CFRunLoopAddObserver(cfRunloop, observer, kCFRunLoopDefaultMode) } // 7 NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "fireTimer", userInfo: nil, repeats: true) var loopCount = 10 repeat { // 8 runloop.runUntilDate(NSDate(timeIntervalSinceNow: 1)) loopCount-- } while(loopCount > 0) } } func observerCallbackFunc() -> CFRunLoopObserverCallBack { return {(observer, activity, context) -> Void in switch { case CFRunLoopActivity.Entry: print("Run Loop已经启动") break case CFRunLoopActivity.BeforeTimers: print("Run Loop分配定时任务前") break case CFRunLoopActivity.BeforeSources: print("Run Loop分配输入事件源前") break case CFRunLoopActivity.BeforeWaiting: print("Run Loop休眠前") break case CFRunLoopActivity.AfterWaiting: print("Run Loop休眠后") break case CFRunLoopActivity.Exit: print("Run Loop退出后") break default: break } } } func fireTimer() { } }let testThread = TestThread()testThread.launch()

下面解读一下上述代码示例,launch()方法在主线程中,通过NSThread类的类方法detachNewThreadSelector:toTarget:withObject:创建并启动一个二级线程,将createAndConfigObserverInSecondaryThread()方法作为事件消息传入该二级线程,这个方法的主要作用就是在二级线程中创建配置Run
Loop观察者并启动Run
Loop,然后让主线程持续3秒,以便二级线程有足够的时间执行任务。

createAndConfigObserverInSecondaryThread()中共有8个关键步骤,下面一一进行说明:

  • 第一步:通过NSRunLoop类的类方法currentRunLoop()获取当前线程的Run
    Loop,这里获取到的Run Loop对象是NSRunLoop对象。
  • 第二步:申明当前对象的变量,至于为什么要这么做,在下一步中会有说明。
  • 第三步:通过Core
    Foundation框架的CFRunLoopObserverContext结构体构造Run
    Loop观察者上下文,大家需要注意前两个参数,我们先看看这个结构体:

public struct CFRunLoopObserverContext { public var version: CFIndex public var info: UnsafeMutablePointer<Void> public var retain: (@convention (UnsafePointer<Void>) -> UnsafePointer<Void>)! public var release: (@convention (UnsafePointer<Void>) -> Void)! public var copyDescription: (@convention (UnsafePointer<Void>) -> Unmanaged<CFString>!)! public init() public init(version: CFIndex, info: UnsafeMutablePointer<Void>, retain: (@convention (UnsafePointer<Void>) -> UnsafePointer<Void>)!, release: (@convention (UnsafePointer<Void>) -> Void)!, copyDescription: (@convention (UnsafePointer<Void>) -> Unmanaged<CFString>!)!)}
  1. version:结构体版本号,必须设置为0。
  2. info:上下文中retainreleasecopyDescription三个回调函数以及Run
    Loop观察者的回调函数所有者对象的指针。在Swift中,UnsafePointer结构体代表C系语言中申明为常量的指针,UnsafeMutablePoinger结构体代表C系语言中申明为非常量的指针,比如说:

C:void functionWithConstArg(const int *constIntPointer);Swift:func functionWithConstArg(constIntPointer: UnsafePointer<Int32>)C:void functionWithNotConstArg(unsigned int *unsignedIntPointer);Swift:func functionWithNotConstArg(unsignedIntPointer: UnsafeMutablePointer<UInt32>)C:void functionWithNoReturnArg(void *voidPointer);Swift:func functionWithNoReturnArg(voidPointer: UnsafeMutablePointer<Void>)
  • 第四步:通过Core
    Foundation框架的CFRunLoopObserverCreate函数创建CFRunLoopObserver对象:

public func CFRunLoopObserverCreate(allocator: CFAllocator!, _ activities: CFOptionFlags, _ repeats: Bool, _ order: CFIndex, _ callout: CFRunLoopObserverCallBack!, _ context: UnsafeMutablePointer<CFRunLoopObserverContext>) -> CFRunLoopObserver!
  1. allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault
  2. activities:该参数配置观察者监听Run
    Loop的哪种运行状态。在示例中,我们让观察者监听Run
    Loop的所有运行状态。
  3. repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
  4. order:观察者优先级,当Run
    Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
  5. callout:观察者的回调函数,在Core
    Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
  6. context:观察者的上下文。
  • 第五步:因为NSRunLoop没有提供操作观察者的接口,所以我们需要getCFRunLoop()方法获取到CFRunLoop对象。
  • 第六步:通过CFRunLoopAddObserver函数向当前线程的Run
    Loop中添加创建好的观察者:

func CFRunLoopAddObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFString!)
  1. rl:当前线程的CFRunLoop对象。
  2. observer:创建好的观察者。
  3. mode:设置将观察者添加到哪个Run Loop模式中。

这里需要注意的是,一个观察者只能被添加到一个Run
Loop中,但是可以被添加到Run Loop中的多个模式中。

  • 第七步:通过Timer事件源向当前线程发送重复执行的定时任务,时间间隔为0.5秒,因为只是为了测试观察者,所以fireTimer()是一个空任务。另外前文中提到过,如果Run
    Loop中没有任何数据源,那么Run
    Loop启动后会立即退出,所以大家可以把这行注释了运行看看会有什么效果。
  • 第八步:通过NSRunLoop对象的runUntilDate(limitDate: NSDate)方法启动Run
    Loop,设置Run
    Loop的运行时长为1秒。这里将其放在一个循环里,最大循环次数为10次,也就是说,如果不考虑主线程的运行时间,该二级线程的Run
    Loop可运行10次。

再来看看观察者的回调方法observerCallbackFunc(),上面在介绍CFRunLoopObserverCreate函数时提到观察者的回调函数是CFRunLoopObserverCallBack重定义的一个闭包,我们来看看这个闭包:

typealias CFRunLoopObserverCallBack = (CFRunLoopObserver!, CFRunLoopActivity, UnsafeMutablePointer<Void>) -> Void

这个闭包没有返回值,第一个参数是触发监听的观察者,第二个参数是观察者监听的Run
Loop运行状态,第三个参数是观察者的运行上下文环境。所以在回调方法中,我们只需要根据第二个参数的值即可判断观察者监听到的Run
Loop状态。大家可以拷贝上面的代码,建一个Command
Application运行看看结果。

Interacting with Core Animation Layers

  • 每个 view 的 layerClass 默认是 CALayer,可以通过重写 layerClass
    方法来返回其他的 layer,例如你使用了大范围的滚动区域,你可以使用
    CATiledLayer
  • view 的 delegate 指向他自身的 layer
  • layer 不接收事件也不参与响应链

创建Runloop

大家知道,一个线程只能执行一个任务,当任务结束后也就意味着这个线程也要结束,频繁的创建线程也是挺消耗资源的一件事,于是就有了常驻线程,前文介绍线程相关概念时也提到过:

简单的来说,RunLoop用于管理和监听异步添加到线程中的事件,当有事件输入时,系统唤醒线程并将事件分派给RunLoop,当没有需要处理的事件时,RunLoop会让线程进入休眠状态。这样就能让线程常驻在进程中,而不会过多的消耗系统资源,达到有事做事,没事睡觉的效果。

如果想要线程不结束,那就要被执行的任务不结束,让被执行的任务不结束显然不靠谱,那么就需要一个机制,能占着线程。该机制就是事件循环机制(Eventloop),体现在代码中就是一个do-while循环,不断的接收事件消息、处理事件、等待新事件消息,除非接收到一个让其退出的事件消息,否则它将一直这么循环着,线程自然就不会结束。Runloop就是管理消息和事件,并提供Eventloop函数的对象,线程执行的任务其实就是在Runloop对象的Eventloop函数里运行。关于Runloop更详细的知识及配置操作在后文中会有讲述。

在Core Foundation框架中,也为我们提供了一系列相关的类和方法为Run
Loop添加Timer事件源,我们一起来看看:

线程之间的交互

虽然我们尽量让每个线程完成独立的任务,但是有些时候我们需要将二级线程中任务的执行结果发送到主线程中进一步进行操作,那么线程之间的交互就不可避免的发生,幸运的是进程中的线程是共享进程空间的,所以实现线程之间的交互也不是那么困难,比如通过发送messages、全局变量、同步策略等都可以实现,在后面的章节中都会有详细介绍。

无规矩不成方圆,做任何事如果乱来,那必定会出现各种问题。因为线程相对比较底层,所以当我们对线程理解的不是特别透彻时直接创建线程,并手动管理线程,势必会出现正确性和性能上的各种问题,所以就有了这节对使用线程的一些建议。

启动Run Loop

在启动Run
Loop前务必要保证已添加一种类型的事件源,原因在前文中已提到多次。在Cocoa框架和Core
Foundation框架中启动Run
Loop大体有三种形式,分别是无条件启动、设置时间限制启动、指定特定模式启动。

NSRunLoop对象的run()方法和Core
Foundation框架中的CFRunLoopRun()函数都是无条件启动Run
Loop的方式。这种方式虽然是最简单的启动方式,但也是最不推荐使用的一个方式,因为这种方式将Run
Loop置于一个永久运行并且不可控的状态,它使Run
Loop只能在默认模式下运行,无法给Run
Loop设置特定的或自定义的模式,而且以这种模式启动的Run
Loop只能通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制停止。

该方式对应的方法是NSRunLoop对象的runUntilDate(_ limitDate: NSDate)方法,在启动Run
Loop时设置超时时间,一旦超时那么Run
Loop则自动退出。该方法的好处是可以在循环中反复启动Run
Loop处理相关任务,而且可控制运行时长。

该方式对应的方法是NSRunLoop对象的runMode(_ mode: String, beforeDate limitDate: NSDate)方法和Core
Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)函数。前者有两个参数,第一个参数是Run
Loop模式,第二个参数仍然是超时时间,该方法使Run
Loop只处理指定模式中的事件源事件,当处理完事件或超时Run
Loop会退出,该方法的返回值类型是Bool,如果返回true则表示Run
Loop启动成功,并分派执行了任务或者达到超时时间,若返回false则表示Run
Loop启动失败。后者有三个参数,前两个参数的作用一样,第三个参数的意思是Run
Loop是否在执行完任务后就退出,如果设置为false,那么代表Run
Loop在执行完任务后不退出,而是一直等到超时后才退出。该方法返回Run
Loop的退出状态:

  • CFRunLoopRunResult.Finished:表示Run
    Loop已分派执行完任务,并且再无任务执行的情况下退出。
  • CFRunLoopRunResult.Stopped:表示Run
    Loop通过CFRunLoopStop(_ rl: CFRunLoop!)函数强制退出。
  • CFRunLoopRunResult.TimedOut:表示Run Loop因为超时时间到而退出。
  • CFRunLoopRunResult.HandledSource:表示Run
    Loop已执行完任务而退出,改状态只有在returnAfterSourceHandled设置为true时才会出现。

终止线程

打个不恰当的比方,人终有一死,或正常生老病死,或非正常出事故意外而亡,前者尚合情合理后者悲痛欲绝。线程也一样,有正常终止结束,也有非正常的强制结束,不管是线程本身还是应用程序都希望线程能正常结束,因为正常结束也就意味着被执行的任务正常执行完成,从而让线程处理完后事随即结束,如果在任务执行途中强制终止线程,会导致线程没有机会处理后事,也就是正常释放资源对象等,这样会给应用程序带来例如内存溢出这类潜在的问题,所以强烈不推荐强制终止线程的做法。

如果确实有在任务执行途中终止线程的需求,那么可以使用Runloop,在任务执行过程中定期查看是否有收到终止任务的事件消息,这样一来可以在任务执行途中判断出终止任务的信号,然后进行终止任务的相关处理,比如保存数据等,二来可以让线程有充分的时间释放资源。

Run Loops是线程中的基础结构,在上文中也提到过,Run
Loops其实是一个事件循环机制,用来分配、分派线程接受到的事件任务,同时可以让线程成为一个常驻线程,即有任务时处理任务,没任务时休眠,且不消耗资源。在实际应用时,Run
Loop的生命周期并不全是自动完成的,还是需要人工进行配置,不论是Cocoa框架还是Core
Foundation框架都提供了Run Loop的相关对象对其进行配置和管理。

注:Core
Foundation框架是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能,比如线程和Run
Loop、端口、Socket、时间日期等。

在所有的线程中,不论是主线程还是二级线程,都不需要显示的创建Run
Loop对象,这里的显示指的是通过任何create打头的方法创建Run
Loop。对于主线程来说,当应用程序通过UIApplicationMain启动时,主线程中的Run
Loop就已经创建并启动了,而且也配置好了。那么如果是二级线程,则需要我们手动先获取Run
Loop,然后再手动进行配置并启动。下面的章节会向大家详细介绍Run
Loop的知识。

注:在二级线程中获取Run
Loop有两种方式,通过NSRunloop的类方法currentRunLoop获取Run
Loop对象(NSRunLoop),或者通过Core
Foundation框架中的CFRunLoopGetCurrent()函数获取当前线程的Run
Loop对象(CFRunLoop)。NSRunLoopCFRunLoop的上层封装。

let nsrunloop = NSRunLoop.currentRunLoop() let cfrunloop = CFRunLoopGetCurrent()
import Foundationclass TestThread: NSObject { func testCFTimerSource() { let cfRunloop = CFRunLoopGetCurrent() var cfRunloopTimerContext = CFRunLoopTimerContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil) let cfRunloopTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.5, 0, 0, cfRunloopTimerCallback(), &cfRunloopTimerContext) CFRunLoopAddTimer(cfRunloop, cfRunloopTimer, kCFRunLoopDefaultMode) CFRunLoopRun() } func cfRunloopTimerCallback() -> CFRunLoopTimerCallBack { return { (cfRunloopTimer, info) -> Void in print("Fire timer...") } }}let testThread = TestThread()testThread.testCFTimerSource()

避免直接创建线程

创建并管理线程在代码层面相对比较复杂和繁琐,一个不留神就会产生一些潜在的问题。OS
X和iOS都提供了较为上层的创建使用线程的API,就是前面提到一些多任务并发执行的解决方案,比如GCD、Operation
objects。使用它们可以帮我们规避在管理线程和处理线程性能方面可能出现的问题,提高多线程操作时的性能和健壮性。

退出Run Loop

退出Run Loop的方式总体来说有三种:

  • 启动Run Loop时设置超时时间。
  • 强制退出Run Loop。
  • 移除Run Loop中的事件源,从而使Run Loop退出。

第一种方式是推荐使用的方式,因为可以给Run
Loop设置可控的运行时间,让它执行完所有的任务以及给观察者发送通知。第二种强制退出Run
Loop主要是应对无条件启动Run
Loop的情况。第三种方式是最不推荐的方式,虽然在理论上说当Run
Loop中没有任何数据源时会立即退出,但是在实际情况中我们创建的二级线程除了执行我们指定的任务外,有可能系统还会让其执行一些系统层面的任务,而且这些任务我们一般无法知晓,所以用这种方式退出Run
Loop往往会存在延迟退出。

Run Loop的事件来源

Run Loop有两个事件来源,一个是Input
source
,接收来自其他线程或应用程序的异步事件消息,并将消息分派给对应的事件处理方法。另一个是Timer
source
,接收定期循环执行或定时执行的同步事件消息,同样会将消息分派给对应的事件处理方法。

图片 2LearnThread-3

上图展示了Run Loop的两类事件来源,以及在Input
source中的两种不同的子类型,它们分别对应着Run
Loop中不同的处理器。当不同的事件源接收到消息后,通过NSRunLooprunUntilDate:方法启动运行Run
Loop,将事件消息分派给对应的处理器执行,一直到指定的时间时退出Run Loop。

Cocoa框架和Core
Foundation框架都提供了创建配置基于端口事件源的类和方法,下面我们来看看如何使用Cocoa框架创建基于端口的事件源以及配置使用该类事件源。

让线程执行有价值的任务

前文中提到过,线程消耗的系统资源不容小视,所以当我们手动创建和管理线程时,尤其要注意这一点。要保证另起线程执行的任务是有意义的、重要的任务,而且该终止的线程要终止,不要让线程有任何空闲时间,以保证系统资源的最优利用。

Run Loop对象的线程安全性

Run Loop对象的线程安全性取决于我们使用哪种API去操作。Core
Foundation框架中的CFRunLoop对象是线程安全的,我们可以在任何线程中使用。Cocoa框架的NSRunLoop对象是线程不安全的,我们必须在拥有Run
Loop的当前线程中操作Run Loop,如果操作了不属于当前线程的Run
loop,会导致异常和各种潜在的问题发生。

Cocoa框架因为是较为高层的框架,所以没有提供操作较为底层的Run
Loop事件源相关的接口和对象,所以我们只能使用Core
Foundation框架中的对象和函数创建事件源并给Run Loop设置事件源。

Run Loop的观察者

Run Loop的观察者可以理解为Run Loop自身运行状态的监听器,它可以监听Run
Loop的下面这些运行状态:

  • Run Loop准备开始运行时。
  • 当Run Loop准备要执行一个Timer Source事件时。
  • 当Run Loop准备要执行一个Input Source事件时。
  • 当Run Loop准备休眠时。
  • 当Run Loop被进入的事件消息唤醒并且还没有开始让处理器执行事件消息时。
  • 退出Run Loop时。

Run Loop的观察者在NSRunloop中没有提供相关接口,所以我们需要通过Core
Foundation框架使用它,可以通过CFRunLoopObserverCreate方法创建Run
Loop的观察者,类型为CFRunLoopObserverRef,它其实是CFRunLoopObserver的重定义名称。上述的那些可以被监听的运行状态被封装在了CFRunLoopActivity结构体中,对应关系如下:

  • CFRunLoopActivity.Entry
  • CFRunLoopActivity.BeforeTimers
  • CFRunLoopActivity.BeforeSources
  • CFRunLoopActivity.BeforeWaiting
  • CFRunLoopActivity.AfterWaiting
  • CFRunLoopActivity.Exit

Run
Loop的观察者和Timer事件类似,可以只使用一次,也可以重复使用,在创建观察者时可以设置。如果只使用一次,那么当监听到对应的状态后会自行移除,如果是重复使用的,那么会留在Run
Loop中多次监听Run Loop相同的运行状态。

使用NSMachPort对象

NSMachPort对象是什么呢?其实就是线程与线程之间通信的桥梁,我们创建一个NSMachPort对象,将其添加至主线程的Run
Loop中,然后我们在二级线程执行的任务中就可以获取并使用该对象向主线程发送消息,也就是说这种方式是将NSMachPort对象在不同线程中相互传递从而进行消息传递的。

因为NSMachPort只能在OS X系统中使用,所以我们需要创建一个OS
X应用的工程我们先来看看代码:

import Cocoaclass ViewController: NSViewController, NSMachPortDelegate { let printMessageId = 1000 override func viewDidLoad() { super.viewDidLoad() let mainThreadPort = NSMachPort() mainThreadPort.setDelegate NSRunLoop.currentRunLoop().addPort(mainThreadPort, forMode: NSDefaultRunLoopMode) let workerClass = WorkerClass() NSThread.detachNewThreadSelector("launchThreadWithPort:", toTarget: workerClass, withObject: mainThreadPort) } // MARK: NSPortDelegate Method func handlePortMessage(message: NSPortMessage) { }}

首先我们看到ViewController类遵循了NSMachPortDelegate协议,因为它要作为NSMachPort的代理类,通过NSMachPortDelegatehandlePortMessage:方法处理来自二级线程的消息。

viewDidLoad方法中我们先是创建了NSMachPort对象的实例,接着设置它的代理,然后使用NSRunLoopaddPort:forMode:方法将创建好的端口对象添加至主线程的Run
Loop中,最后通过NSThreaddetachNewThreadSelector:toTarget:withObject:方法创建二级线程,并让该二级线程执行WorkerClass类中的launchThreadWithPort:方法,同时将刚才创建好的端口对象作为参数传给该方法,也就是将主线程中的端口对象传到了二级线程中。下面来看看handlePortMessage:中应该如何处理接收到的消息:

func handlePortMessage(message: NSPortMessage) { let messageId = message.msgid if messageId == UInt32(printMessageId) { print("Receive the message that id is 1000 and this is a print task.") } else { // Handle other messages } }

通过端口传递的消息可以根据消息编号判断该执行什么样的任务,所以该方法中通过NSPortMessage对象获取到消息id然后进行判断并执行相应的任务,消息id在二级线程通过端口向主线程发送消息时可以设置。

首先二级线程中与主线程中一样,都需要创建端口对象、设置代理、将端口对象添加至当前线程的Run
Loop中:

import Cocoaclass WorkerClass: NSObject, NSMachPortDelegate { func launchThreadWithPort(port: NSMachPort) { autoreleasepool{ let secondaryThreadPort = NSMachPort() secondaryThreadPort.setDelegate let runloop = NSRunLoop.currentRunLoop() runloop.addPort(secondaryThreadPort, forMode: NSDefaultRunLoopMode) sendPrintMessage(port, receivePort: secondaryThreadPort) runloop.runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 500)) } } func sendPrintMessage(sendPort: NSMachPort, receivePort: NSMachPort) { } // MARK: NSPortDelegate Method func handlePortMessage(message: NSPortMessage) { } }

创建并配置好端口后就需要向主线程发送消息了,下面我们来看看sendPrintMessage:receivePort:方法:

func sendPrintMessage(sendPort: NSMachPort, receivePort: NSMachPort) { let portMessage = NSPortMessage(sendPort: sendPort, receivePort: receivePort, components: nil) portMessage.msgid = UInt32 portMessage.sendBeforeDate(NSDate(timeIntervalSinceNow: 1)) }

首先需要创建NSPortMessage对象,该对象就是端口之间相互传递的介质,初始化方法的第一个参数为主线程的端口对象,也就是发送消息的目标端口,第二个参数是二级线程的端口对象,第三个参数的作用是向主线程发送需要的数据,该参数的类型是AnyObject的数组。

创建完消息对象后,要给该消息设置消息id,以便主线程接收后进行判断,最后通过sendBeforeDate:方法发送消息。

在前文中提到过,在应用中使用多线程势必会给增加我们编写代码的工作量,而且会带来一些潜在的问题,最大的问题就是资源竞争的问题,多个线程同时访问资源或者重复更改资源。如果我们足够幸运,这些问题会使应用产生比较明显的异常现象,那我们尚可发现并修复,但是如果这些问题产生的影响不那么明显,或者说只有在应用做一些特定操作才会发生异常,而我们又没测到时就会给我们带来大麻烦。

或许我们可以让每个线程之间都不进行交互,没个线程都有独有资源,从而避免资源竞争问题的发生,但是这并不是长远之计,很多情况下线程之间必须要进行交互,这时我们就需要更好的设计模式或者工具策略来避免这类问题的发生。所幸的是OS
X和iOS系统已经提供了多种线程安全的方法,这一节让我们来看看如何使用它们。

避免资源竞争

进程中的线程是共享该进程空间的,所以很容易出现多个线程对同一个变量进行操作从而导致程序执行结果错误的情况。如果为每个线程都提供一份变量的拷贝,的确是可以解决这个问题,但是在开发中这样会造成更大的弊端,所以前文中提到了一些同步策略,能帮助我们达到线程交互及解决资源竞争的目的。但是在理论上还是会有出错的可能,比如让线程在指定的顺序下对某个变量依次进行操作。所以在程序设计阶段应该尽量避免线程之间的资源竞争及减少线程之间的交互。

创建Run Loop事件源对象

我们定义自己的Run
Loop事件源首先就是需要创建事件源,我们来看看创建事件源的方法:

func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
  1. allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault
  2. order:事件源优先级,当Run
    Loop中有多个接收相同事件的事件源被标记为待执行时,那么就根据该优先级判断,0为最高优先级别。
  3. context:事件源上下文。

Run Loop事件源上下文很重要,我们来看看它的结构:

struct CFRunLoopSourceContext { var version: CFIndex var info: UnsafeMutablePointer<Void> var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)! var release: ((UnsafePointer<Void>) -> Void)! var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)! var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)! var hash: ((UnsafePointer<Void>) -> CFHashCode)! var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! var perform: ((UnsafeMutablePointer<Void>) -> Void)! init() init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!) }

该结构体中我们需要关注的是前两个和后三个属性:

  1. version:事件源上下文的版本,必须设置为0。
  2. info:上下文中retainreleasecopyDescriptionequalhashschedulecancelperform这八个回调函数所有者对象的指针。
  3. schedule:该回调函数的作用是将该事件源与给它发送事件消息的线程进行关联,也就是说如果主线程想要给该事件源发送事件消息,那么首先主线程得能获取到该事件源。
  4. cancel:该回调函数的作用是使该事件源失效。
  5. perform:该回调函数的作用是执行其他线程或当前线程给该事件源发来的事件消息。

Run Loop Modes

Run Loop Modes可以称之为Run Loop模式,这个模式可以理解为对Run
Loop各种设置项的不同组合,举个例子,iPhone手机运行的iOS有很多系统设置项,假设白天我打开蜂窝数据,晚上我关闭蜂窝数据,而打开无线网络,到睡觉时我关闭蜂窝数据和无线网络,而打开飞行模式。假设在这三个时段中其他的所有设置项都相同,而只有这三个设置项不同,那么就可以说我的手机有三种不同的设置模式,对应着不同的时间段。那么Run
Loop的设置项是什么呢?那自然就是前文中提到的不同的事件来源以及观察者了,比如说,Run
Loop的模式A,只包含接收Timer Source事件源的事件消息以及监听Run
Loop运行时的观察者,而模式B只包含接收Input
Source事件源的事件消息以及监听Run Loop准备休眠时和退出Run
Loop时的观察者,如下图所示:

图片 3LearnThread-4

所以说,Run Loop的模式就是不同类型的数据源和不同观察者的集合,当Run
Loop运行时要设置它的模式,也就是告知Run
Loop只需要关心这个集合中的数据源类型和观察者,其他的一概不予理会。那么通过模式,就可以让Run
Loop过滤掉它不关心的一些事件,以及避免被无关的观察者打扰。如果有不在当前模式中的数据源发来事件消息,那只能等Run
Loop改为包含有该数据源类型的模式时,才能处理事件消息。

在Cocoa框架和Core Foundation框架中,已经为我们预定义了一些Run Loop模式:

  • 默认模式:在NSRunloop中的定义为NSDefaultRunLoopMode,在CFRunloop中的定义为kCFRunLoopDefaultMode。该模式包含的事件源囊括了除网络链接操作的大多数操作以及时间事件,用于当前Run
    Loop处于空闲状态等待事件时,以及Run Loop开始运行时。
  • NSConnectionReplyMode:该模式用于监听NSConnection相关对象的返回结果和状态,在系统内部使用,我们一般不会使用该模式。
  • NSModalPanelRunLoopMode:该模式用于过滤在模态面板中处理的事件。
  • NSEventTrackingRunLoopMode:该模式用于跟踪用户与界面交互的事件。
  • 模式集合:或者叫模式组,顾名思义就是将多个模式组成一个组,然后将模式组认为是一个模式设置给Run
    Loop,在NSRunloop中的定义为NSRunLoopCommonModes,在CFRunloop中的定义为kCFRunLoopCommonModes。系统提供的模式组名为Common
    Modes,它默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode这三个模式。

以上五种系统预定的模式中,前四种属于只读模式,也就是我们无法修改它们包含的事件源类型和观察者类型。而模式组我们可以通过Core
Foundation框架提供的CFRunLoopAddCommonMode(_ rl: CFRunLoop!, _ mode: CFString!)方法添加新的模式,甚至是我们自定义的模式。这里需要注意的是,既然在使用时,模式组是被当作一个模式使用的,那么自然可以给它设置不同类型的事件源或观察者,当给模式组设置事件源或观察者时,实际是给该模式组包含的所有模式设置。比如说给模式组设置了一个监听Run
Loop准备休眠时的观察者,那么该模式组里的所有模式都会被设置该观察者。

原子操作(Atomic Operations)

原子操作是最简单也是最基本的保证线程安全的方法,原子的本意是不能被分裂的最小粒子,故原子操作是不可被中断的一个或一系列操作。从处理器角度来说原子操作是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址,从应用层面来说就是当一个线程对共享变量进行操作时,其他线程不能对该变量进行操作,并且其他线程不会被阻塞。

举个简单的例子,有一个共享变量i,初始值是1,现在我们对它进行两次i++的操作,期望值是3,但是在多核CPU的情况下就有可能是CPU1对i进行了一次i++操作,CPU2对i进行了一次i++操作,所以结果就并不是我们期望的值3,而是2,因为CPU1和CPU2同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。在我们使用原子操作时首先应将变量申明为原子类型(atomic_t),然后根据内核提供的原子操作API对变量进行操作,比如给原子类型的变量v增加值i的函数void atomic_add(int i, atomic_t *v);等。OS
X和iOS也提供了一些数学运算和逻辑运算的原子操作供我们使用,这里就不深入说明了,大家如果有兴趣可以去官方文档找找。

用户界面与线程

用户界面的更新、对用户事件的响应都应该放在主线程中,避免线程不安全的情况,以及能方便的管理UI界面。目前Cocoa框架默认对UI的操作都要在主线程中完成,即使不强制要求,我们也应该这样做。但是有一些情况比较特殊,比如对图片的处理,因为处理图片的过程并不是显性的,所以处理的过程可以放在二级线程中,当处理完成后,再在主线程中显示结果。这样可以有效的提升应用的性能。

将事件源添加至Run Loop

事件源创建好之后,接下来就是将其添加到指定某个模式的Run
Loop中,我们来看看这个方法:

func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
  1. rl:希望添加事件源的Run Loop对象,类型是CFRunLoop
  2. source:我们创建好的事件源。
  3. mode:Run Loop的模式。

我们再来看看这个方法都干了些什么:

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { ..... __CFRunLoopSourceSchedule(rls, rl, rlm); .....}static void __CFRunLoopSourceSchedule(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) { ..... if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.schedule) { rls->_context.version0.schedule(rls->_context.version0.info, rl, rlm->_name); } } ..... }

从上述的代码片段可以看出,在CFRunLoopAddSource中调用了__CFRunLoopSourceSchedule内部函数,而该函数中正是执行了Run
Loop事件源上下文中的schedule回调函数。也就是说当把事件源添加到Run
Loop中后就会将事件源与给它发送事件消息的线程进行关联。

Input Source

前文中说过,Input
Sources接收到各种操作输入事件消息,然后异步的分派给对应事件处理方法。在Input
Sources中又分两大类的事件源,一类是基于端口事件源(Port-based
source),在CFRunLoopSourceRef的结构中为source1,主要通过监听应用程序的Mach端口接收事件消息并分派,该类型的事件源可以主动唤醒Run
Loop。另一类是自定义事件源(Custom
source),在CFRunLoopSourceRef的结构中为source0,一般是接收其他线程的事件消息并分派给当前线程的Run
Loop,比如performSwlwctor:onThread:...系列方法,该类型的事件源无法自动唤醒Run
Loop,而是需要手动将事件源设置为待执行的标记,然后再手动唤醒Run
Loop。虽然这两种类型的事件源接收事件消息的方式不一样,但是当接收到消息后,对消息的分派机制是完全相同的。

Cocoa框架和Core
Foundation框架都提供了相关的对象和函数用于创建基于端口的事件源。在Cocoa框架中,实现基于端口的事件源主要是通过NSPort类实现的,它代表了交流通道,也就是说在不同的线程的Run
Loop中都存在NSPort,那么它们之间就可以通过发送与接收消息(NSPortMessage)互相通信。所以我们只需要通过NSPort类的类方法port创建对象实例,然后通过NSRunloop的方法将其添加到Run
Loop中,或者在创建二级线程时将创建好的NSPort对象传入即可,无需我们再做消息、消息上下文、事件源等其他配置,都由Run
Loop自行配置好了。而在Core
Foundation框架中就比较麻烦一些,大多数配置都需要我们手动配置,在后面会详细举例说明。

Cocoa框架中没有提供创建自定义事件源的相关接口,我们只能通过Core
Foundation框架中提供的对象和函数创建自定义事件源,手动配置事件源各个阶段要处理的逻辑,比如创建CFRunLoopSourceRef事件源对象,通过CFRunLoopScheduleCallBack回调函数配置事件源上下文并注册事件源,通过CFRunLoopPerformCallBack回调函数处理接收到事件消息后的逻辑,通过CFRunLoopCancelCallBack函数销毁事件源等等,在后文中会有详细举例说明。

虽然Cocoa框架没有提供创建自定义事件源的相关对象和接口,但是它为我们预定义好了一些事件源,能让我们在当前线程、其他二级线程、主线程中执行我们希望被执行的方法,让我们看看NSObject中的这些方法:

func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool)func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)

这两个方法允许我们将当前线程中对象的方法让主线程去执行,可以选择是否阻塞当前线程,以及希望被执行的方法作为事件消息被何种Run
Loop模式监听。

注:如果在主线程中使用该方法,当选择阻塞当前线程,那么发送的方法会立即被主线程执行,若选择不阻塞当前线程,那么被发送的方法将被排进主线程Run
Loop的事件队列中,并等待执行。

func performSelector(_ aSelector: Selector, withObject anArgument: AnyObject?, afterDelay delay: NSTimeInterval)func performSelector(_ aSelector: Selector, withObject anArgument: AnyObject?, afterDelay delay: NSTimeInterval, inModes modes: [String])

这两个方法允许我们给当前线程发送事件消息,当前线程接收到消息后会依次加入Run
Loop的事件消息队列中,等待Run
Loop迭代执行。该方法还可以指定消息延迟发送时间及消息希望被何种Run
Loop模式监听。

注:该方法中的延迟时间并不是延迟Run
Loop执行事件消息的事件,而是延迟向当前线程发送事件消息的时间。另外,即便不设置延迟时间,那么发送的事件消息也不一定立即被执行,因为在Run
Loop的事件消息队列中可以已有若干等待执行的消息。

func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool)func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)

这两个方法允许我们给其他二级线程发送事件消息,前提是要取得目标二级线程的NSThread对象实例,该方法同样提供了是否阻塞当前线程的选项和设置Run
Loop模式的选项。

注:使用该方法给二级线程发送事件消息时要确保目标线程正在运行,换句话说就是目标线程要有启动着的Run
Loop。并且保证目标线程执行的任务要在应用程序代理执行applicationDidFinishLaunching:方法前完成,否则主线程就结束了,目标线程自然也就结束了。

func performSelectorInBackground(_ aSelector: Selector, withObject arg: AnyObject?)

该方法允许我们在当前应用程序中创建一个二级线程,并将指定的事件消息发送给新创建的二级线程。

class func cancelPreviousPerformRequestsWithTarget(_ aTarget: AnyObject)class func cancelPreviousPerformRequestsWithTarget(_ aTarget: AnyObject, selector aSelector: Selector, object anArgument: AnyObject?)

这两个方法是NSObject的类方法,第一个方法作用是在当前线程中取消Run
Lop中某对象通过performSelector:withObject:afterDelay:方法发送的所有事件消息执行请求。第二个方法多了两个过滤参数,那就是方法名称和参数,取消指定方法名和参数的事件消息执行请求。

内存屏障(Memory Barriers)和可见变量(Volatile Variables)

CPU对内存的操作无非就是读和写,我们虽然知道CPU对内存进行了操作,但是我们无法决定在一系列CPU对内存的操作时单个操作指令的顺序,这些顺序完全由CPU随性而来。举个例子,在有两个CPU的情况下,现在有四个指令待操作:

A = 1; x = A;B = 2; y = B;

这四个指令的执行顺序就可能有24种不同的组合。所以内存屏障就是一个帮助CPU规定操作指令顺序的手段,它将内存操作隔开,给屏障两侧的内存操作强加一个顺序关系,比如所有该屏障之前的写操作和读操作必须在该屏障之后的写操作和读操作之前执行。

可见变量是另一个确保共享变量被多个线程操作后仍能保持正确结果的机制,CPU为了提高处理速度,通常情况下不会直接与主存打交道,而是先将系统主存中的数据读到缓存中,当从缓存中读取到共享变量,对其进行操作后又不会立即写回主存,所以如果其他CPU也要操作该共享变量,就很有可能读到它的旧值。但是当我们在申明共享变量时加上volatile关键字,将其申明为可见变量时就可以避免这种情况,因为CPU从缓存中读取并修改可见共享变量后会立即写回主存,而且其他CPU在操作之前会先判断缓存中的数据是否已过期,如果过期那么从主存中重新缓存,这样一来可见变量在每个CPU操作时都能保证是最新值。但需要注意的是内存屏障和可见变量都会降低编译器的性能,所以没有必须要使用的情况时不要滥用这两个机制。

清楚当线程结束时应该做什么

当用户退出应用后,理论上该应用进程中的所有线程都会立即被结束。但是如果此时正好有一个二级线程在后台处理其他任务,比如说下载或者正在存储一些数据。那么此时就要判断正在处理的这些任务是否要保留,如果要丢弃,那么直接结束所有线程即可,但是如果要保留,那么就需要主线程等待正在处理任务的二级线程,从而延迟应用退出。

这里处理时有两种情况,如果自行创建的线程并手动管理,那么要使用POSIX
API创建具有joinable特性的二级线程,使主线程与之相关联。如果是使用Cocoa框架,那么可以使用applicationShouldTerminate:代理方法延迟应用关闭,当二级线程处理完任务后回调replyToApplicationShouldTerminate:通知到主线程,然后关闭应用。

标记事件源及唤醒Run Loop

前面的文章中说过,srouce0类型,也就是非port类型的事件源都需要进行手动标记,标记完还需要手动唤醒Run
Loop,下面我们来看看这两个方法:

func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)func CFRunLoopWakeUp(_ rl: CFRunLoop!)

这里需要注意的是唤醒Run Loop并不等价与启动Run Loop,因为启动Run
Loop时需要对Run Loop进行模式、时限的设置,而唤醒Run
Loop只是当已启动的Run Loop休眠时重新让其运行。

Timer Source

Timer Source顾名思义就是向Run
Loop发送在将来某一时间执行或周期性重复执行的同步事件消息。当某线程不需要其他线程通知而需要自己通知自己执行任务时就可以用这种事件源。举个应用场景,在iOS应用中,我们经常会用到搜索功能,而且一些搜索框具有自动搜索的能力,也就是说不用我们点击搜索按钮,只需要输入完我想要搜索的内容就会自动搜索,大家想一想如果每输入一个字就开始立即搜索,不但没有意义,性能开销也大,用户体验自然也很糟糕,我们希望当输入完这句话,或至少输入一部分之后再开始搜索,所以我们就可以在开始输入内容时向执行搜索功能的线程发送定时搜索的事件消息,让其在若干时间后再执行搜索任务,这样就有缓冲时间输入搜索内容了。

这里需要注意的是Timer Source发送给Run
Loop的周期性执行任务的重复时间是相对时间。比如说给Run
Loop发送了一个每隔5秒执行一次的任务,每次执行任务的正常时间为2秒,执行5次后终止,假设该任务被立即执行,那么当该任务终止时应该历时30秒,但当第一次执行时出现了问题,导致任务执行了20秒,那么该任务只能再执行一次就终止了,执行的这一次其实就是第5次,也就是说不论任务的执行时间延迟与否,Run
Loop都会按照初始的时间间隔执行任务,并非按Finish-To-Finish去算的,所以一旦中间任务有延时,那么就会丢失任务执行次数。关于Timer
Source的使用,在后文中会有详细举例说明。

锁机制

锁机制在大多数编程语言中都是很常用的线程安全机制,你可以在关键的代码前后,或者只希望同时只能被一个线程执行的任务前后加上线程锁来避免因为多线程给程序造成不可预知的问题。OS
X和iOS提供了多种锁的类型,下面让我们来看一看:

  • 互斥锁:互斥锁扮演的角色就是代码或者说任务的栅栏,它将你希望保护的代码片段围起来,当其他线程也试图执行这段代码时会被互斥锁阻塞,直到互斥锁被释放,如果多个线程同时竞争一个互斥锁,有且只有一个线程可以获得互斥锁。
  • 递归锁(Recursive
    lock):递归锁是互斥锁的变种。它允许一个线程在已经拥有一个锁,并且没有释放的前提下再次获得锁。当该线程释放锁时也需要一个一个释放。
  • 读写锁(Read-write
    lock):读写锁一般用在有资源被多个线程频繁的进行读操作,而只偶尔会有专职线程对该资源进行写操作的情况下。读写锁可被多个进行读操作的线程获得,但只能被一个进行写操作的线程获得,当有读操作的线程等待时,写操作的线程就不能获得锁,反之亦然,当写操作的线程在等待时,读操作的线程就不能获得锁。
  • 分配锁(Distributed
    lock):这种锁作用在进程级别,将进程保护起来,但是该锁不会阻塞其他进程,而是当其他进程与被保护进程交互时分配锁会告知前来的访问进程被访问进程处于锁状态,让前来访问的进程自行决定下一个操作。
  • 自旋锁(Spin
    lock):自旋锁与互斥锁有点类似,但不同的是其他线程不会被自旋锁阻塞,而是而是在进程中空转,就是执行一个空的循环。一般用于自旋锁被持有时间较短的情况。
  • 双检测锁(Double-checked
    lock):这种锁的目的是为了最大限度推迟上锁的时间,因为在多线程中线程安全对开销还是挺大的,所以一般能不上锁就不上锁。所以这种锁在上锁之前会先检查一次是否需要上锁,在上锁之后再检查一次,最后才真正执行操作。

异常处理

每个线程都有捕获当前任务在执行时产生的异常的责任,不论是主线程还是二级线程。如果二级线程产生的异常需要交由主线程处理是也不能任由其抛出,而是先将其捕获,然后向主线程发送消息,告知主线程当前的情况。当消息发出后二级线程可根据需求选择继续处理其他的任务还是终止线程。

执行Run Loop事件源的任务

唤醒Run Loop意味着让休眠的Run Loop重新运行,那么我们就从启动Run
Loop,让其开始运行的方法看起:

extension NSRunLoop { ..... public func runUntilDate(limitDate: NSDate) { while runMode(NSDefaultRunLoopMode, beforeDate: limitDate) && limitDate.timeIntervalSinceReferenceDate > CFAbsoluteTimeGetCurrent() { } } public func runMode(mode: String, beforeDate limitDate: NSDate) -> Bool { ..... let limitTime = limitDate.timeIntervalSinceReferenceDate let ti = limitTime - CFAbsoluteTimeGetCurrent() CFRunLoopRunInMode(modeArg, ti, true) return true }}SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);}SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { ..... result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, false); ..... return result;}static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, Boolean waitIfEmpty) { ..... __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); ..... }static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) { CFTypeRef sources = NULL; ..... if (__CFRunLoopSourceIsSignaled { ..... rls->_context.version0.perform(rls->_context.version0.info); ..... } .....}

从上述代码片段中可以看出,当Run
Loop运行后会调用内部函数__CFRunLoopDoSources0执行自定义事件源的任务,在执行之前会通过内部函数__CFRunLoopSourceIsSignaled判断事件源是否已被标记为待执行,然后执行Run
Loop事件上下文中的perform回调函数。

Run Loop内部运行逻辑

在Run
Loop的运行生命周期中,无时无刻都伴随着执行等待执行的各种任务以及在不同的运行状态时通知不同的观察者,下面我们看看Run
Loop中的运行逻辑到底是怎样的:

  1. 通知对应观察者Run Loop准备开始运行。
  2. 通知对应观察者准备执行定时任务。
  3. 通知对应观察者准备执行自定义事件源的任务。
  4. 开始执行自定义事件源任务。
  5. 如果有基于端口事件源的任务准备待执行,那么立即执行该任务。然后跳到步骤9继续运转。
  6. 通知对应观察者线程进入休眠。
  7. 如果有下面的事件发生,则唤醒线程:
  • 接收到基于端口事件源的任务。
  • 定时任务到了该执行的时间点。
  • Run Loop的超时时间到期。
  • Run Loop被手动唤醒。
  1. 通知对应观察者线程被唤醒。
  2. 执行等待执行的任务。
  • 如果有定时任务已启动,执行定时任务并重启Run
    Loop。然后跳到步骤2继续运转。
  • 如果有非定时器事件源的任务待执行,那么分派执行该任务。
  • 如果Run Loop被手动唤醒,重启Run Loop。然后跳转到步骤2继续运转。
  1. 通知对应观察者已退出Run Loop。

以上这些Run
Loop中的步骤也不是每一步都会触发,举一个例子:1.对应观察者接收到通知Run
Loop准备开始运行 -> 3.对应观察者接收到通知Run
Loop准备执行自定义事件源任务 -> 4.开始执行自定义事件源任务 ->
任务执行完毕且没有其他任务待执行 ->
6.线程进入休眠状态,并通知对应观察者 -> 7.接收到定时任务并唤醒线程
-> 8.通知对应观察者线程被唤醒 -> 9.执行定时任务并重启Run Loop
-> 2.通知对应观察者准备执行定时任务 -> Run
Loop执行定时任务,并在等待下次执行任务的间隔中线程休眠 ->
6.线程进入休眠状态,并通知对应观察者…

这里需要注意的一点是从上面的运行逻辑中可以看出,当观察者接收到执行任务的通知时,Run
Loop并没有真正开始执行任务,所以观察者接收到通知的时间与Run
Loop真正执行任务的时间有时间差,一般情况下这点时间差影响不大,但如果你需要通过观察者知道Run
Loop执行任务的确切时间,并根据这个时间要进行后续操作的话,那么就需要通过结合多个观察者接收到的通知共同确定了。一般通过监听准备执行任务的观察者、监听线程进入休眠的观察者、监听线程被唤醒的观察者共同确定执行任务的确切时间。

Conditions

Conditions是一种多线程间协调通信的机制,它通常用于标明共享资源是否可被访问或者确保一系列任务能按照指定的执行顺序执行。如果一个线程试图访问一个共享资源,而正在访问该资源的线程将其条件设置为不可访问,那么该线程会被阻塞,直到正在访问该资源的线程将访问条件更改为可访问状态或者说给被阻塞的线程发送信号后,被阻塞的线程才能正常访问这个资源。后面会说明如何使用这种机制。

诚然使用线程安全的各种机制可以是我们的程序更加健壮,不易出错,但是因为这些机制本身也会有较大的性能开销,如果滥用这些机制反而会严重影响到程序的性能。所以我们应该在线程安全和性能之间寻求到一个平衡点,这一节我们就来看看在设计线程安全时应该注意的事项。

尽可能少的使用常驻线程

前文中提到过,可以为一些经常需要执行的、具有周期性的、量级较小的任务创建常驻线程,以减少创建关闭线程的资源消耗,但是不能滥用常驻线程。理论上,一个线程执行完任务后就应该关闭,并且关闭线程的最佳时机是执行完任务的后一秒。目的是为了避免空闲线程占用过多的资源从而导致一些潜在的问题。

移除Run Loop事件源

当我们自定义的事件源完成使命后就可以将其从Run
Loop中移除,我们来看看对应的方法:

func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { ..... __CFRunLoopSourceCancel(rls, rl, rlm); .....}static void __CFRunLoopSourceCancel(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) { if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.cancel) { rls->_context.version0.cancel(rls->_context.version0.info, rl, rlm->_name); } } ..... }

从上述代码片段可以看出,当我们调用了CFRunLoopRemoveSource方法后,其实是执行了Run
Loop事件源上下文中的cancel回调函数。

避免滥用线程安全机制

不论是新的项目还是已经有的项目,在设计逻辑代码或者属性时应该避免产生线程安全与不安全的问题。有效的避免措施就是减少逻辑代码之间的交互,或者说任务与任务之间的交互,线程与线程之间的交互,减少多线程中任务访问同一变量的情况,如果需要那么可以确保每个任务中都有该变量的拷贝,这样就可以有效避免对变量或者任务采取线程安全机制。虽然对变量进行拷贝也会消耗资源,但是我们应该要判断一下这与采用线程安全机制消耗的资源之间谁多谁少,从而做出正确的决定。

确保类库的线程安全

如果我们在开发应用的相关功能,我们完全可以控制这块功能是否需要多线程去完成,但是当我们在开发一个供别人使用的类库时,就没法灵活的控制了。所以只能假设使用我们的类库必定会在多线程的环境中使用,这样我们可以通过锁机制确保线程安全。但是如果我们的类库没有在多线程环境中使用呢?那就会白白浪费掉对锁进行操作的相关资源,只能说使用锁机制可以保证类库线程安全的万无一失,但性能方面会大打折扣。

另一种方式是让使用我们类库的应用要对类库进行明确地初始化,不管是主线程还是二级线程,换句话说也就是让每个线程都有一份我们类库的内容,这样也可以有效的保证类库线程安全。在Cocoa框架中,还有一种可选的方式,就是可以为NSWillBecomeMultiThreadedNotification注册一个观察者,目的是当应用变为多线程环境时可以通知到我们的类库,从而采取相关措施,但这种方式不保险,有可能当类库已经被多线程环境中的代码使用后才收到通知。总而言之,如果开发类库,那么务必要确保其线程安全。

在OS
X和iOS中,每个应用其实就是一个进程,一个进程中由一个或多个线程组成,每个线程代表了所属应用中代码的执行路径。通常情况下应用始于主线程中的主函数,当需要有其他功能在二级线程中与主线程并行执行时,便可以创建其他二级线程。

一旦二级线程被创建,那么它就是一个独立的实体,线程与线程之间是没有任何关联的,它们有各自的执行堆栈,由内核单独为每个线程分派运行时的执行任务。虽然每个线程是独立实体,但是它们之间是可以相互交互的,在实际的应用中,这类需求是很常见的,因为它们共享所属进程的内存空间,并且拥有相同的读写权,所以也很容易实现线程之间的交互。既然一个应用中可能会有多个线程协作完成功能,所以管理线程就是重中之重了,这一章节会从线程的资源消耗、创建、配置、使用、关闭这几个关键点梳理实际运用中的线程管理。

线程的资源消耗主要分为三类,一类是内存空间的消耗、一类是创建线程消耗的时间、另一类是对开发人员开发成本的消耗。

内存空间的消耗又分为两部分,一部分是内核内存空间,另一部分是应用程序使用的内存空间,每个线程在创建时就会申请这两部分的内存空间。申请内核内存空间是用来存储管理和协调线程的核心数据结构的,而申请应用程序的内存空间是用来存储线程栈和一些初始化数据的。对于用户级别的二级线程来说,对应用程序内存空间的消耗是可以配置的,比如线程栈的空间大小等。下面是两种内存空间通常的消耗情况:

  • 内核内存空间:主要存储线程的核心数据结构,每个线程大约会占用1KB的空间。
  • 应用程序内存空间:主要存储线程栈和初始化数据,主线程在OS
    X中大约占8MB空间,在iOS中大约占1MB。二级线程在两种系统中通常占大约512KB,但是上面提到二级线程在这块是可以配置的,所以可配置的最小空间为16KB,而且配置的空间大小必须是4KB的倍数。

注意:二级线程在创建时只是申请了内存程序空间,但还并没有真正分配给二级线程,只有当二级线程执行代码需要空间时才会真正分配。

线程的创建时间取决于机器硬件的性能,但通常大约在90毫秒,虽然在我们看来90毫秒很短,但当频繁的创建线程时就会影响到CPU处理其他任务的时间。所以现在往往都会使用线程池,避免频繁的创建全新的线程。

前文中提到过设计和开发多线程的应用较单线程要复杂的多,要注意的事项在上文中就提出了八条,针对每条注意事项,都要花费不少时间去设计代码和测试。所以总体来说如果涉及到多线程,务必会增加开发人员的开发测试时间,但是换来的是应用程序具有更好的健壮性和高性能,所谓慢工出细活。

说到创建线程,就得说说线程的两种类型,JoinableDetach。Joinable类型的线程可以被其他线程回收其资源和终止。举个例子,如果一个Joinable的线程与主线程结合,那么当主线程准备结束而该二级线程还没有结束的时候,主线程会被阻塞等待该二级线程,当二级线程结束后由主线程回收其占用资源并将其关闭。如果在主线程还没有结束时,该二级线程结束了,那么它不但不会关闭,而且资源也不会被系统收回,只是等待主线程处理。而Detach的线程则相反,会自行结束关闭线程并且有系统回收其资源。

在OS
X和iOS系统中有多种创建线程的方法,不同方法创建出的线程可能会有不同的线程属性,但就线程本身来说并没有什么差异。下面来看看创建线程的不同方法。

自定义Run Loop事件源的实际运用

在讲解示例之前,我们先来看看示例Demo的效果:

图片 4LearnThread-5

在这个示例中,创建了两个自定义事件源,一个添加到主线程中,另一个添加到二级线程中。主线程给二级线程中的自定义事件源发送事件消息,目的是让其改变所有UICollectionViewCell的透明度,当二级线程收到事件消息后执行计算每个UICollectionViewCell透明度的任务,然后再给主线程的自定义事件源发送事件消息,让其更新UICollectionViewCell的透明度并显示。下面来看看类图:

图片 5LearnThread-6

整个工程一共就这六个类:

  • MainCollectionViewController:程序主控制器,启动程序、展示UI及计算UICollectionViewCell透明度的相关方法。
  • MainThreadRunLoopSource:主线程自定义事件源管理对象,负责初始化事件源,将事件源添加至指定线程,标记事件源并唤醒指定Run
    Loop以及包含上文中说过的事件源最主要的三个回调方法。
  • MainThreadRunLoopSourceContext:主线程自定义事件源上下文,可获取到对应的事件源及添加了该事件源的Run
    Loop。
  • SecondaryThreadRunLoopSource:二级线程自定义事件源管理对象,负责初始化事件源,将事件源添加至指定线程,标记事件源并唤醒指定Run
    Loop以及包含上文中说过的事件源最主要的三个回调方法。
  • SecondaryThreadRunLoopSourceContext:二级线程自定义事件源上下文,可获取到对应的事件源及添加了该事件源的Run
    Loop。
  • AppDelegate:应用程序代理类,这里零时充当为各自定义事件源回调方法执行内容的管理类。

下面我按照程序的运行顺序一一对这些类及属性和方法进行简单说明。

MainCollectionViewController类中与UI展示相关的方法在这里就不再累赘了。点击Start按钮,调用start()方法,初始化MainThreadRunLoopSource对象,在这个过程中初始化了CFRunLoopSourceContext对象并且创建CFRunLoopSource对象以及初始化该事件源的指令池:

let mainThreadRunLoopSource = MainThreadRunLoopSource() mainThreadRunLoopSource.addToCurrentRunLoop()

var runloopSourceContext = CFRunLoopSourceContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil, equal: nil, hash: nil, schedule: runloopSourceScheduleRoutine(), cancel: runloopSourceCancelRoutine(), perform: runloopSourcePerformRoutine runloopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &runloopSourceContext) commandBuffer = Array<SecondaryThreadRunLoopSourceContext>()

这里需要注意的是CFRunLoopSourceContextinit方法中的第二个参数和CFRunLoopSourceCreate方法的第三个参数都是指针,那么在Swift中,将对象转换为指针的方法有两种:

  • 使用unsafeBitCast方法,该方法会将第一个参数的内容按照第二个参数的类型进行转换。一般当需要对象与指针来回转换时使用该方法。
  • 在对象前面加&符号,表示传入指针地址。

当主线程的自定义事件源初始化完成之后,调用addToCurrentRunLoop()方法,将事件源添加至当前Run
Loop中,即主线程的Run Loop:

let cfrunloop = CFRunLoopGetCurrent() if let rls = runloopSource { CFRunLoopAddSource(cfrunloop, rls, kCFRunLoopDefaultMode) }

接下来创建二级线程,并且让其执行二级线程的配置任务:

let secondaryThread = NSThread(target: self, selector: "startThreadWithRunloop", object: nil) secondaryThread.start()

在二级线程中同样初始化自定义事件源,并将将其添加至二级线程的Run
Loop中,然后启动Run Loop:

func startThreadWithRunloop() { autoreleasepool{ var done = false let secondaryThreadRunLoopSource = SecondaryThreadRunLoopSource() secondaryThreadRunLoopSource.addToCurrentRunLoop() repeat { let result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, true) if ((result == CFRunLoopRunResult.Stopped) || (result == CFRunLoopRunResult.Finished)) { done = true; } } while } }

前文中说过将事件源添加至Run
Loop后会触发事件源的schedule回调函数,所以当执行完mainThreadRunLoopSource.addToCurrentRunLoop()这句代码后,便会触发主线程自定义事件源的schedule回调函数:

func runloopSourceScheduleRoutine() -> @convention (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void { return { (info, runloop, runloopMode) -> Void in let mainThreadRunloopSource = unsafeBitCast(info, MainThreadRunLoopSource.self) let mainThreadRunloopSourceContext = MainThreadRunLoopSourceContext(runloop: runloop, runloopSource: mainThreadRunloopSource) let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("registerMainThreadRunLoopSource:", withObject: mainThreadRunloopSourceContext) } }

这里还需注意的是在Swift2.0中,如果一个作为回调函数方法的返回类型是指向函数的指针,这类指针可以转换为闭包,并且要在闭包前面加上@convention标注。在runloopSourceScheduleRoutine()方法中,获取到主线程事件源对象并初始化事件源上下文对象,然后将该事件源上下文对象传给AppDelegate的对应方法注册该事件源上下文对象:

func registerMainThreadRunLoopSource(runloopSourceContext: MainThreadRunLoopSourceContext) { mainThreadRunloopSourceContext = runloopSourceContext }

自然当在二级线程中执行完secondaryThreadRunLoopSource.addToCurrentRunLoop()这句代码后,也会触发二级线程自定义事件源的schedule回调函数:

func runloopSourceScheduleRoutine() -> @convention (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void { return { (info, runloop, runloopMode) -> Void in let secondaryThreadRunloopSource = unsafeBitCast(info, SecondaryThreadRunLoopSource.self) let secondaryThreadRunloopSourceContext = SecondaryThreadRunLoopSourceContext(runloop: runloop, runloopSource: secondaryThreadRunloopSource) let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelectorOnMainThread("registerSecondaryThreadRunLoopSource:", withObject: secondaryThreadRunloopSourceContext, waitUntilDone: true) } }

这里要注意的是,在该方法中同样是将二级线程事件源上下文对象传给了AppDelegate的对应方法,但是这里用了performSelectorOnMainThread方法,让其在主线程中执行,目的在于注册完上下文对象后就接着从主线程给二级线程发送事件消息了,其实我将这里作为了主线程触发二级线程执行任务的触发点:

func registerSecondaryThreadRunLoopSource(runloopSourceContext: SecondaryThreadRunLoopSourceContext) { secondaryThreadRunloopSourceContext = runloopSourceContext sendCommandToSecondaryThread() } func sendCommandToSecondaryThread() { secondaryThreadRunloopSourceContext?.runloopSource?.commandBuffer?.append(mainThreadRunloopSourceContext!) secondaryThreadRunloopSourceContext?.runloopSource?.signalSourceAndWakeUpRunloop(secondaryThreadRunloopSourceContext!.runloop!) }

从上述代码中可以看到在sendCommandToSecondaryThread()方法中,将主线程的事件源上下文放入了二级线程事件源的指令池中,这里我设计的是只要指令池中有内容就代表事件源需要执行后续任务了。然后执行了二级线程事件源的signalSourceAndWakeUpRunloop()方法,给其标记为待执行,并唤醒二级线程的Run
Loop:

func signalSourceAndWakeUpRunloop(runloop: CFRunLoopRef) { CFRunLoopSourceSignal(runloopSource) CFRunLoopWakeUp }

当二级线程事件源被标记并且二级线程Run
Loop被唤醒后,就会触发事件源的perform回调函数:

func runloopSourcePerformRoutine() -> @convention (UnsafeMutablePointer<Void>) -> Void { return { info -> Void in let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("performSecondaryThreadRunLoopSourceTask") } }

二级线程事件源的perform回调函数会在当前线程,也就是二级线程中执行AppDelegate中的对应方法:

func performSecondaryThreadRunLoopSourceTask() { if secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 { mainCollectionViewController!.generateRandomAlpha() let mainThreadRunloopSourceContext = secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer![0] secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll() mainThreadRunloopSourceContext.runloopSource?.commandBuffer?.append(secondaryThreadRunloopSourceContext!) mainThreadRunloopSourceContext.runloopSource?.signalSourceAndWakeUpRunloop(mainThreadRunloopSourceContext.runloop!) } }

从上述代码中可以看到,先会判断二级线程事件源的指令池中有没有内容,如果有的话,那么执行计算UICollectionViewCell透明度的任务,然后从指令池中获取到主线程事件源上下文对象,将二级线程事件源上下文对象放入主线程事件源的指令池中,并将主线程事件源标记为待执行,然后唤醒主线程Run
Loop。之后便会触发主线程事件源的perform回调函数:

func runloopSourcePerformRoutine() -> @convention (UnsafeMutablePointer<Void>) -> Void { return { info -> Void in let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("performMainThreadRunLoopSourceTask") } }

func performMainThreadRunLoopSourceTask() { if mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 { mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll() mainCollectionViewController!.collectionView.reloadData() let timer = NSTimer(timeInterval: 1, target: self, selector: "sendCommandToSecondaryThread", userInfo: nil, repeats: false) NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode) } }

performMainThreadRunLoopSourceTask()方法中同样会先判断主线程事件源的指令池是否有内容,然后执行MainCollectionViewController中的刷新UI的方法,最后再次给二级线程发送事件消息,以此循环。大家可以去Github下载该示例的源码,编译环境是Xcode7.2,然后可以自己试着在界面中添加一个Stop按钮,让事件源执行cancel回调函数。

认清使用线程安全机制时的陷阱

在使用锁机制和内存屏障机制时我们往往需要考虑将它们设置在代码的哪个位置是最正确的,但是有些时候,你认为正确的位置不代表它真的正确,下面是一段伪代码片段,向我们揭示一个使用锁机制时容易发生的陷阱。假设有一个可变类型的数组myArray,但是该数组中的对象是不可变类型的对象anObject

NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray(); id anObject;[arrayLock lock]; anObject = [myArray objectAtIndex:0]; [arrayLock unlock];[anObject doSomething];

上述代码片段中,对从myArray数组中获取第一个元素的操作加了锁,因为该数组是可变类型的,所以加锁防止其他线程同时操作该数组从而导致错误发生,又因为anObject是一个不可变类型对象,所以不需要担心其他线程会对其进行改变,所以调用anObject对象的doSomething方法时并没有加锁。

看起来这段代码的逻辑似乎没什么问题,但是凡事都架不住如果和万一,如果在arrayLock释放锁之后和anObject对象调用doSomething方法之前这区间里,另外一个线程清空了myArray里的元素,这时这段代码的结果会怎样呢?答案显然是因为当前类对anObject对象的引用被释放,anObject对象因为指向了错误的内存地址从而调用方法出错。所以为了避免这种小概率事件的发生,应该将anObject对象调用方法的操作也加上锁:

NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray();id anObject;[arrayLock lock]; anObject = [myArray objectAtIndex:0]; [anObject doSomething]; [arrayLock unlock];

那么问题又来了,如果doSomething方法执行的时间很长,线程锁一直无法释放,那么又会对线程的性能产生很大影响。要想彻底解决问题,就要找到产生问题的关键点,在这个示例中产生问题的关键点就是anObject对象有可能被其他线程释放,所以解决问题的关键就是防止anObject对象被释放,我们来看看最终的解决方案:

NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray(); id anObject;[arrayLock lock];anObject = [myArray objectAtIndex:0]; [anObject retain]; [arrayLock unlock];[anObject doSomething]; [anObject release];

使用NSThread创建线程

使用NSThread创建线程有两种方式:

  • detachNewThreadSelector:toTarget:withObject::该方法是一个类方法,适用于OS
    X所有的版本和iOS2.0之后的版本。该方法其实完成了两个动作,先是创建线程,然后启动线程。通过方法名称就可以得知,该方法创建的线程为Detach类型的线程。
  • 创建NSThread对象:这种方法适用于OS X
    10.5之后的版本和iOS2.0之后的版本。该方法通过创建NSThread对象,使用它的start()方法启动线程,该方法的好处是可以在启动前通过NSThread对象的各个属性进行配置,待配置妥当后再调用start()方法启动线程。该方法创建的线程也是Detach类型的线程。

该方法有三个参数:

  • selector:发送给线程的消息,或者说是让线程执行的任务。这里需要注意的是该任务最多只能有一个参数,并且不能有返回值。
  • target:在新的线程中接收消息的对象。
  • object:传给target对象的参数,也就是传入selector中的参数。

下面来看一个简单示例:

import Foundationclass TestThread { func launch() { print("First event in Main Thread.") NSThread.detachNewThreadSelector("methodInSecondaryThread:", toTarget: self, withObject: "I am a argument") print("Second event in Main Thread.") } func methodInSecondaryThread(arg: String) { print of event in Secondary Thread.") } }let testThread = TestThread()testThread.launch()

上述代码定义了一个类TestThread,包含两个方法launch()methodInSecondaryThread()lanch()方法中用print()函数模拟事件,在两个事件中创建一个二级线程,用于执行methodInSecondaryThread()方法,在该方法中执行其他事件。执行看看结果如何:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSThread initWithTarget:selector:object:]: target does not implement selector (*** -[LearnThread.TestThread methodInSecondaryThread])'

结果很不幸,报错了,原因很简单,因为我们的代码是Swift,而NSThread继承了NSObject是Objective-C世界的东西,所以需要对代码进行修改,有两种方法:

// 1. 让NSTread继承NSObjectclass TestThread: NSObject {// 2. 在methodInSecondaryThread()方法前添加@objc@objc func methodInSecondaryThread(arg: String) {

我习惯让类继承NSObject

import Foundationclass TestThread: NSObject { func launch() { print("First event in Main Thread.") NSThread.detachNewThreadSelector("methodInSecondaryThread:", toTarget: self, withObject: "I am a argument") print("Second event in Main Thread.") } func methodInSecondaryThread(arg: String) { print of event in Secondary Thread.") } }let testThread = TestThread()testThread.launch()

继续运行看看效果:

First event in Main Thread.Second event in Main Thread.

运行成功了,但似乎少点什么东西,methodInSecondaryThread()方法中的内容并没有打印出来,难道线程没有执行吗?我们通过Instruments可以看到,在运行过程中二级线程是创建过的:

图片 6LearnThread-1

导致这个问题的原因和上文介绍的线程类型有关系。因为主线程运行很快,快到当主线程结束时我们创建的二级线程还没来得及执行methodInSecondaryThread()方法,而通过detachNewThreadSelector:toTarget:withObject:创建的二级线程是Detach类型的,没有与主线程结合,所以主线程也不会等待,当主线程结束,进程结束,二级线程自然也结束了。解决这个问题的办法就是让二级线程有执行任务的时间,所以我们可以让主线程停顿几秒,让二级线程完成它的任务:

import Foundationclass TestThread: NSObject { func launch() { print("First event in Main Thread.") NSThread.detachNewThreadSelector("methodInSecondaryThread:", toTarget: self, withObject: "I am a argument") sleep print("Second event in Main Thread.") } func methodInSecondaryThread(arg: String) { print of event in Secondary Thread.") } }let testThread = TestThread()testThread.launch()

再运行就可以看到正确地结果了:

First event in Main Thread.I am a argument of event in Secondary Thread.Second event in Main Thread.

我们可以通过initWithTarget:selector:object:方法实例化一个NSThread对象,该方法的三个参数其实与detachNewThreadSelector:toTarget:withObject:方法的参数一样,只是顺序不一样而已:

import Foundationclass TestThread: NSObject { func launch() { print("First event in Main Thread.") let secondaryThread = NSThread(target: self, selector: "methodInSecondaryThread:", object: "I am a argument") secondaryThread.start() sleep print("Second event in Main Thread.") } func methodInSecondaryThread(arg: String) { print of event in Secondary Thread.") } }let testThread = TestThread()testThread.launch()

上述的代码的运行结果自然也是一样的:

First event in Main Thread.I am a argument of event in Secondary Thread.Second event in Main Thread.

这种方法依然只能在二级线程中执行最多只有一个参数的函数或方法,如果想要执行多参数的任务,可以将参数放入集合中传递,当然被执行的任务得能正确接收到参数集合。或者可以通过另外一种方法,那就是通过创建继承NSThread的类,然后重写main()方法来实现:

import Foundationclass CustomThread: NSThread { var arg1: String! var arg2: String! init(arg1: String, arg2: String) { self.arg1 = arg1 self.arg2 = arg2 } override func main() { print("\(self.arg1), \(self.arg2), we are the arguments in Secondary Thread.") } }class TestThread: NSObject { func launch() { print("First event in Main Thread.") let customThread = CustomThread(arg1: "I am arg1", arg2: "I am arg2") customThread.start() sleep print("Second event in Main Thread.") } func methodInSecondaryThread(arg: String) { print of event in Secondary Thread.") } }let testThread = TestThread()testThread.launch()

如上述代码所示,我们创建了CustomThread类,并继承了NSThread,然后通过初始化方法传参,再重写main()方法处理相关任务。执行结果如下:

First event in Main Thread.I am arg1, I am arg2, we are the arguments in Secondary Thread.Second event in Main Thread.

防止死锁和活锁的发生

死锁的意思就是线程A和线程B各持有一把锁,现在线程A在等待线程B释放锁,而线程B又在等待线程A释放锁,所以这两个线程谁也拿不到锁,也不是释放自己持有的锁,就会永远被阻塞在进程中。

活锁的意思是线程A可以使用资源,但它很礼貌,让其他线程先使用资源,线程B也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源,导致活锁,活锁与死锁的区别在于前者的线程并没有被阻塞,而是在不停的做一些与任务无关的事。

产生死锁和活锁的根本原因是线程中持有多把锁,所以避免这两种情况发生的最好办法就是尽量让线程只持有一把锁,如果实在有需求要持有多把锁,那么也应该尽量避免其他线程来请求锁。

使用NSObject创建线程

在OS
X和iOS中,NSObject对象本身就具有创建线程的能力,所以只要是继承了NSObject的类自然也具备这个能力:

import Foundationclass TestThread: NSObject { func launch() { print("First event in Main Thread.") performSelectorInBackground("performInBackground", withObject: nil) sleep print("Second event in Main Thread.") } func performInBackground() { print("I am a event, perform in Background Thread.") } }let testThread = TestThread()testThread.launch()

上述代码中的TestThread类继承了NSObject类,那么就可以通过performSelectorInBackground:withObject:方法创建二级线程,该方法只有两个参数:

  • selector:发送给线程的消息,或者说是让线程执行的任务。这里需要注意的是该任务最多只能有一个参数,并且不能有返回值。
  • object:传给target对象的参数,也就是传入selector中的参数。

该方法创建的线程也是Detach类型的。以上这几种方式都是基于Cocoa框架实现的,大家可以使用NSThread的类方法isMultiThreaded去检验,在合适的地方插入这行代码print(NSThread.isMultiThreaded,看看程序的线程状态。

正确使用volatile关键字

如果你已经使用的锁机制来保护一段代码逻辑,那么就不要使用volatile关键字来保护这段代码中使用的变量。上文中说过,可见变量机制会让代码每次从主存中加载读取变量而非缓存,本身就比较影响性能,如果再与锁机制结合,不但没有起到额外的保护作用,反而会严重影响程序的性能。所以如果使用了锁机制,那么可以完全省去使用可见变量机制,因为锁机制就已经可以很好的保护变量的线程安全性了,不需要多此一举。

有些时候我们只希望一些数学运算或者简单的逻辑能够保证线程安全,如果使用锁机制或者条件机制虽然可以实现,但是会耗费较大的资源开销,并且锁机制还会使线程阻塞,造成性能损失,非常不划算,所以当遇到这种情况时,我们可以尝试使用原子操作来达到目的。

我们一般使用原子操作对32位和64位的值执行一些数学运算或简单的逻辑运算,主要依靠底层的硬件指令或者使用内存屏障确保正在执行的操作是线程安全的,下面我们来看看Apple给我们提供了哪些原子操作的方法:

使用POSIX API创建线程

在OS X和iOS中,可以通过POSIX
API创建线程,上文中提到过,POSIX的线程API实际是基于C语言的线程接口,这些接口在使用线程和配置线程方面更加容易和灵活,移植性也比较强,但由于相对较为底层,如果不熟悉C语言,上手成本会比较高,NSThread就是基于POSIX线程API封装而成的。

POSIX
API通过int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine), void *restrict arg);函数创建线程:

  • thread:线程标识符。
  • attr:线程属性设置。
  • start_routine:线程函数的起始地址。
  • arg:传递给start_routine的参数。
  • 返回值:成功返回0,出错返回-1。

大体的参数其实和使用NSThread创建线程基本一致,不过需要注意的是通过pthread_create()创建的线程是Joinable类型的,如果要将新线程设置为Detach类型,需要在创建前使用pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);函数设置其线程属性。

在Cocoa框架中,上文提到的那些同步机制,比如线程锁,当二级线程创建后才就会自动生成。如果在程序中使用POSIX
API创建线程,那么Cocoa框架是无法得知当前程序已处于多线程状态的,所以就不会自动开启相关的同步机制,而当我们又没有通过POSIX
API手动控制的话,就有可能导致应用程序崩溃的情况。另外要注意的一点是Cocoa框架中的线程锁是不能操作通过POSIX
API创建的线程的,反之亦然。所以当Cocoa框架与POSIX
API混用的时候,在同步机制方面一定要配套使用。

Add操作

Add操作是将两个整数相加,并将结果存储在其中一个变量中:

  • OSAtomicAdd32(__theAmount: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicAdd32Barrier(__theAmount: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicAdd64(__theAmount: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicAdd64Barrier(__theAmount: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Int64

var num: Int64 = 10OSAtomicAdd64(20, &num) OSAtomicAdd64Barrier(20, &num) print // 50

Increment操作

Increment操作将指定值加1:

  • OSAtomicIncrement32(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicIncrement32Barrier(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicIncrement64(__theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicIncrement64Barrier(__theValue: UnsafeMutablePointer<Int64>) -> Int64

var num: Int64 = 10OSAtomicIncrement64 OSAtomicIncrement64Barrier print // 12

Decrement操作

Decrement操作将指定值减1:

  • OSAtomicDecrement32(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicDecrement32Barrier(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicDecrement64(__theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicDecrement64Barrier(__theValue: UnsafeMutablePointer<Int64>) -> Int64

var num: Int64 = 10OSAtomicDecrement64 OSAtomicDecrement64Barrier print // 8

OR逻辑运算、AND逻辑运算、XOR逻辑运算

对两个32位数值中的位置相同的位执行按位比较:

  • OSAtomicOr32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicOr32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicAnd32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicAnd32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicXor32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicXor32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32

CAS操作

CAS操作是比较与交换(Compare and
Swap)操作,有三个参数分别是旧值、新值、想要比较的值的内存地址,整个过程是先将你期望的旧值与指定的内存地址中的值进行比较,如果相同,那么将该内存地址的值更新为指定的新值,并返回true,如果比较后发现不同,那么不再做任何操作,并返回false,Apple提供了不同类型的CAS原子操作:

  • OSAtomicCompareAndSwap32(__oldValue: Int32, _ __newValue: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Bool
  • OSAtomicCompareAndSwap64(__oldValue: Int64, _ __newValue: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Bool
  • OSAtomicCompareAndSwapPtr(__oldValue: UnsafeMutablePointer<Void>, _ __newValue: UnsafeMutablePointer<Void>, _ __theValue: UnsafeMutablePointer<UnsafeMutablePointer<Void>>) -> Bool
  • OSAtomicCompareAndSwapLong(__oldValue: Int, _ __newValue: Int, _ __theValue: UnsafeMutablePointer<Int>) -> Bool

var num: Int64 = 10let result = OSAtomicCompareAndSwap64(10, 20, &num)print // 20print // truevar num: Int64 = 10let result = OSAtomicCompareAndSwap64(11, 20, &num)print // 10print // false

比特位设置操作

将给定比特位的值设置位1或者0:

  • OSAtomicTestAndSet(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndSetBarrier(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndClear(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndClearBarrier(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool

锁机制是多线程编程中最常用的也是最基本的确保线程安全的机制,它能有效的保证多行逻辑代码的线程安全性。OS
X和iOS系统为我们提供了基本的互斥锁和基于互斥锁变异的特殊锁以应对不同的情况。这一节我们来看看如何使用锁机制。

POSIX互斥锁

前文中说过,POSIX是可移植操作系统接口(Portable Operating System
Interface of
UNIX),它定义了操作系统应该为应用程序提供的接口标准,在类Unix系统中都可以使用。使用POSIX互斥锁很简单,先申明互斥锁指针,类型为UnsafeMutablePointer<pthread_mutex_t>,然后通过pthread_mutex_init函数初始化互斥锁,最后通过pthread_mutex_lock函数和pthread_mutex_unlock函数上锁和释放锁:

class TestLock { let mutex: UnsafeMutablePointer<pthread_mutex_t> init() { mutex = UnsafeMutablePointer.alloc(sizeof(pthread_mutex_t)) } func posixMutexLock() { pthread_mutex_init(mutex, nil) pthread_mutex_lock print("Do work...") pthread_mutex_unlock } }let textLock = TestLock()textLock.posixMutexLock()

使用NSLock

在Cocoa框架中,我们可以使用NSLock来实现锁机制,该类遵循了NSLocking协议,并实现了加锁和释放锁的方法。

NSLock中有两个加锁的方法:

  • tryLock:该方法使当前线程试图去获取锁,并返回布尔值表示是否成功,但是当获取锁失败后并不会使当前线程阻塞。
  • lockBeforeDate:该方法与上面的方法类似,但是只有在设置的时间内获取锁失败线程才不会被阻塞,如果获取锁失败时已超出了设置的时间,那么当前线程会被阻塞。

class TestLock { let nslock: NSLock init() { nslock = NSLock() } func acquireLock() { nslock.tryLock() // nslock.lockBeforeDate(NSDate(timeIntervalSinceNow: 10)) print("Do work...") nslock.unlock() } }let textLock = TestLock()textLock.acquireLock()

使用NSRecursiveLock

上文中介绍了几种锁的类型,其中一种叫递归锁,在Cocoa中对应的类是NSRecursiveLock,我们来看看如何使用:

class TestLock { let nsRecursiveLock: NSRecursiveLock init() { nsRecursiveLock = NSRecursiveLock() } func recursiveFunction(var value: Int) { nsRecursiveLock.lock() if value != 0 { --value print") recursiveFunction } nsRecursiveLock.unlock() } }let textLock = TestLock()textLock.recursiveFunction

使用NSConditionLock

条件锁也是互斥锁的一种变种,在Cocoa框架中对应的类是NSConditionLock,条件锁顾名思义可以设置加锁和释放锁的条件。假设我们有一个消息队列,并且有消息生产者和消息消费者,那么一般情况是当消息生产者产生消息,放入消息队列,然后消息消费者从消息队列中获取消息,并将其从消息队列移除进行后续操作。那么消费者在获取消息和移除消息时要确保两点先决条件,第一就是获取消息时队列中确实已有消息,第二就是此时生产者不能向队列中添加消息,否则会影响消息队列中消息的顺序或者影响获取到消息的结果,所以在这种情况下我们就可以使用条件锁来保证他们的线程安全:

class TestLock { let nsConditionLock: NSConditionLock var messageQueue = [AnyObject]() let HAS_MESSAGES = 1 let NO_MESSAGES = 0 init() { nsConditionLock = NSConditionLock(condition: NO_MESSAGES) } func produceMessage() { NSThread.detachNewThreadSelector("consumeMessage", toTarget: self, withObject: nil) while true { nsConditionLock.lock() // 生产消息并添加到消息队列中 nsConditionLock.unlockWithCondition(HAS_MESSAGES) } } func consumeMessage() { while true { nsConditionLock.lockWhenCondition(HAS_MESSAGES) // 从消息队列中获取消息并从队列中移除消息 nsConditionLock.unlockWithCondition(messageQueue.isEmpty ? NO_MESSAGES : HAS_MESSAGES) } } }let textLock = TestLock()textLock.produceMessage()

使用@synchronized关键字

在Objective-C中,我们会经常使用@synchronized关键字来修饰变量,确保变量的线程安全,它能自动为修饰的变量创建互斥锁或解锁:

- myMethod:anObj { @synchronized { // 在该作用域中,anObj不会被其他线程改变 }}

从上面的代码片段中可以看到myMethod:方法的anObj参数在被@synchronized关键字修饰的作用域中是线程安全的。而且使用该关键字还有一个好处,那就是当有多个线程要同时执行一个带参数的方法,但不同线程中传递的参数不同,如果用NSLock将该方法中的逻辑代码上锁,那么就只能有一个线程获得锁,而其他线程就会被阻塞,如果使用@synchronized关键字就可以避免其他线程被阻塞的情况。

但在Swift中,Apple不知出于什么考虑,这个关键字已经不存在了,也就是我们不能在Swift中使用这个关键字对变量加锁了,但关键字都是语法糖,虽然不能使用语法糖,但还是可以使用其背后的机制的,我们来看看objc_sync的源码,看看这个关键字都干了些什么:

// Begin synchronizing on 'obj'. // Allocates recursive mutex associated with 'obj' if needed.// Returns OBJC_SYNC_SUCCESS once lock is acquired. int objc_sync_enter{ int result = OBJC_SYNC_SUCCESS; if  { SyncData* data = id2data(obj, ACQUIRE); assert; data->mutex.lock(); } else { // @synchronized does nothing if (DebugNilSync) { _objc_inform("NIL SYNC DEBUG: @synchronized; set a breakpoint on objc_sync_nil to debug"); } objc_sync_nil(); } return result;}// End synchronizing on 'obj'. // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERRORint objc_sync_exit{ int result = OBJC_SYNC_SUCCESS; if  { SyncData* data = id2data(obj, RELEASE); if  { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } else { bool okay = data->mutex.tryUnlock(); if  { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } } } else { // @synchronized does nothing } return result;}

可见@synchronized关键字其实是调用了objc_sync_enterobjc_sync_exit这两个方法,所以在Swift中使用时可以这样给变量加锁:

func myMethod(anObj: AnyObject!) { objc_sync_enter // anObj参数在这两个方法之间具有线程安全特性,不会被其他线程改变 objc_sync_exit }

Condition机制和锁机制很类似,区别也不大,同样都会使线程阻塞,这一节我们来看看如何使用该机制。

使用NSCondition类

这里举个生产者和消费者的例子,消费者从队列中获取产品进行消费,当队列中没有产品时消费者等待生产者生产,当生产者生产出产品放入队列后再通知消费者继续进行消费:

class TestLock { var products: [AnyObject] let nscondition: NSCondition init() { products = [AnyObject]() nscondition = NSCondition() NSThread.detachNewThreadSelector("consumeProduct", toTarget: self, withObject: nil) NSThread.detachNewThreadSelector("generateProduct", toTarget: self, withObject: nil) } func consumeProduct() { nscondition.lock() guard products.count == 0 else { nscondition.wait() } let product = products[0] products.removeAtIndex print nscondition.unlock() } func generateProduct() { nscondition.lock() let product = NSObject() products.append print nscondition.signal() nscondition.unlock() } }

从上面代码中可以看到,NSCondition类同样是用lockunlock方法进行上锁和释放锁,然后通过wait方法阻塞线程,通过signal方法唤醒阻塞的线程,该方法唤醒的时最近一次使用wait方法等待的线程。如果想一次性唤醒所有在等待的线程,可以使用broadcast方法。NSCondition还有另外一个阻塞线程的方法waitUntilDate(_ limit: NSDate),该方法设置一个线程阻塞时间并返回一个布尔值,如果在指定的时间内没有信号量的通知,那么就唤醒线程继续进行,此时该方法返回false,如果在指定时间内接收到信号量的通知,此时该方法返回true

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图