iOS进阶之路 (十五)多线程 - 基础

本篇主要涉及多线程的基础知识,内容相对简单,为接下来的GCD、锁作好铺垫。html

一. 进程 & 线程 & 任务

1.1 进程 -- process

  • 进程是指在系统中正在运行的一个应用程序。
  • 每一个进程之间是独立的,每一个进程均运行在其专用的且受保护的内存

补充:iOS系统是相对封闭的系统,App在各自的沙盒(sandbox)中运行,每一个App都只能读取iPhone上系统为该应用程序程序建立的文件夹AppData下的内容,不能随意跨越本身的沙盒去访问别的App沙盒中的内容。也就是说OS是单进程的,一个App就是一个进程。程序员

1.2 线程 - thread

  • 线程进程 的基本执行单元,一个 进程 的全部任务都在 线程 中执行
  • 进程 要想执行任务,至少要有一条 线程
  • 程序启动会默认开启一条 线程,这条线程被称为主线程UI线程

补充:对于iOS开发来讲,线程的底层实现是基于 POSIX threads API 的,也就是咱们常说的 pthreads编程

1.3 任务:task

  • 通俗的说任务就是就一件事情或一段代码,线程其实就是去执行这件事情。

1.4 进程与线程的关系

  • 地址空间: 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,可是进程之间 的资源是独立的。
  • 一个进程崩溃后,在保护模式下不会对其余进程产生影响,可是一个线程崩溃整个进程 都死掉。因此多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。因此涉及到频繁的切换时,使用线程要好于进程。 一样若是要求同时进行而且又要共享某些变量的并发操做,只能用线程不能用进程
  • 执行过程:每一个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。可是线 程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,可是进程不是。

二. 线程和runloop的关系

苹果不容许直接建立 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:缓存

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    /// 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;
     
    /// 获取一个 pthread 对应的 RunLoop。
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
        OSSpinLockLock(&loopsLock);
        
        if (!loopsDic) {
            // 第一次进入时,初始化全局Dic,并先为主线程建立一个 RunLoop。
            loopsDic = CFDictionaryCreateMutable();
            CFRunLoopRef mainLoop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
        }
        
        /// 直接从 Dictionary 里获取。
        CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
        
        if (!loop) {
            /// 取不到时,建立一个
            loop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, thread, loop);
            /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
            _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
        }
        
        OSSpinLockUnLock(&loopsLock);
        return loop;
    }
     
    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
     
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
复制代码
  • 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里, key 是 pthread_t, value 是 CFRunLoopRef。
  • RunLoop 的建立是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)
  • 主线程runloop 程序一启动就默认建立好了,默认开启
  • 子线程runloop 只有当咱们使用的时候才会建立,默认关闭,因此在子线程调用runloop方法要开启runloop。

三. 多线程

在同一时刻,一个CPU只能处理1条线程,但CPU能够在多条线程之间快速的切换,只要切换的足够快,就形成了多线程一同执行的假象。安全

3.1 多线程的意义

  1. 优势
  • 能适当提升程序的执行效率
  • 能适当提升资源的利用率(CPU,内存)
  • 线程上的任务执行完成后,线程会自动销毁
  1. 缺点
  • 开启线程须要占用必定的内存空间(默认状况下,每个线程都占 512 KB)
  • 若是开启大量的线程,会占用大量的内存空间,下降程序的性能 * 线程越多,CPU 在调用线程上的开销就越大
  • 程序设计更加复杂,好比线程间的通讯、多线程的数据共享

3.2 多线程的生命周期

  • 新建:实例化线程对象
  • 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
  • 运行:CPU 负责调度可调度线程池中线程的执行。线程执行完成以前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
  • 阻塞:当知足某个预约条件时,可使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
  • 死亡:正常死亡,线程执行完毕。非正常死亡,当知足某个条件后,在线程内部停止执行/在主线程停止线程对象

3.3 线程池 - thread pool

线程池是一种线程使用模式。 线程过多会带来调度开销,进而影响缓存局部性和总体性能。 而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。 这避免了在处理短期任务时建立与销毁线程的代价。bash

线程池的执行流程如图下:多线程

  1. 线程池大小 小于 核心线程池大小,建立线程执行任务
  2. 线程池大小 大于等于 核心线程池大小,则判断线程池工做队列是否已满
  • 若没满就将任务提交给工做队列
  • 若已满时,将建立新的线程来执行任务;反之则交给 饱和策略 去处理。

饱和策略:并发

  • AbortPolicy 直接抛出RejectedExecutionExeception 异常来阻止系统正常运行
  • CallerRunsPolicy 将任务回退到调用者
  • DisOldestPolicy 丢掉等待最久的任务‘
  • DisCardPolicy 直接丢弃任务
  • 这四种拒绝策略均实现的RejectedExecutionHandler接口

因此在并发的时候,同时能有多少个线程在运行是由线程池的线程缓存数量决定。GCD和NSOperation的线程池缓存数量都是64条app

3.4 多线程的实现方案

  • GCD仅仅支持FIFO队列,不支持异步操做之间的依赖关系设置。而NSOperation中的队列能够被从新设置优先级,从而实现不一样操做的执行顺序调整
  • NSOperation支持KVO,能够观察任务的执行状态
  • GCD更接近底层,GCD在追求性能的底层操做来讲,是速度最快的
  • 从异步操做之间的事务性,顺序行,依赖关系。GCD须要本身写更多的代码来实现,而NSOperation已经内建了这些支持
  • 若是异步操做的过程须要更多的被交互和UI呈现出来,NSOperation更好;底层代码中,任务之间不太互相依赖,而须要更高的并发能力,GCD则更有优点。

四. 线程的同步

线程编程的危害之一是在多个线程之间的资源争夺。若是多个线程在同一个时间试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源,并确保每一个线程都有在它操做的资源上面的独特设置。由于保持彻底独立的资源是不可行的,因此你可能必须使用锁,条件,原子操做和其余技术来同步资源的访问。异步

咱们看一下苹果官方给出的线程同步工具:

4.1 Atomic Operations -- 原子操做

Atomic Operations是一种基于基本数据类型的同步形式,底层用汇编锁来控制变量的变化,保证数据的正确性,好处在于不会block互相竞争的线程,且相比锁耗时不多。

4.2 Memory Barriers -- 内存屏障

为了达到最佳性能,编译器一般会讲汇编级别的指令进行从新排序,从而保持处理器的指令管道尽量的满。做为优化的一部分,编译器可能会对内存访问的指令进行从新排序(在它认为不会影响数据的正确性的前提下),然而,这并不必定都是正确的,顺序的变化可能致使一些变量的值获得不正确的结果。

Memory Barriers是一种不会形成线程block的同步工具,它用于确保内存操做的正确顺序。Memory Barriers像一道屏障,迫使处理器在其前面完成必须的加载或者存储的操做。Memory Barriers常被用于确保一个线程中可被其余线程访问的内存操做按照预期的顺序执行。具体参考Memory Barriers。

在程序中应用Memory Barriers只须要在指定地方调用:

OSMemoryBarrier();
复制代码

4.3 Volatile Variables -- 挥发变量

Volatile Variables是另一种针对变量的同步工具。众所周知,CPU访问寄存器的速度比访问内存速度快不少,所以,CPU有时候会将一些变量放置到寄存器中,而不是每次都从内存中读取(例如for循环中的i值)从而优化代码,可是可能会致使错误。 例如,一个线程在CPUA中被处理,CPUA从内存获取变量F的值,此时,并无其余CPU用到变量F,因此CPUA将变量F存到寄存器中,方便下次使用,此时,另外一个线程在CPUB中被处理,CPUB从内存中获取变量F的值,改变该值后,更新内存中的F值。可是,因为CPUA每次都只会从寄存器中取F的值,而不会再次从内存中取,因此,CPUA处理后的结果就是不正确的。

对一个变量加上Volatile关键字能够迫使编译器每次都从新从内存中加载该变量,而不会从寄存器中加载。当一个变量的值可能随时会被一个外部源改变时,应该将该变量声明为Volatile。

4.4 Locks -- 锁

Locks是一种最经常使用的同步工具。Locks能够对一段代码进行保护,保证同时只有一个线程在执行该段代码。

关于iOS开发中的各类锁的性能和使用,咱们后续单独开一个篇章详细学习。

4.5 Conditions -- 条件

Conditions是一种特殊的lock,用于同步操做的顺序。与Mutex Lock不一样的是,一个等待Condition的线程保持block,直到另外一个线程显示对该Condition调用signal。

因为操做系统的缘由,Conditions可能会获得一些不正确的信号,为了不这类问题,能够在使用Conditions时,加入Predicate(断言)。Predicate是一种有效地判断是否让一个线程处理信号的方式。Conditions保持线程休眠,直到另外一个线程调用signal,并设置了Predicate。

4.6 perform selector routines

cocoa应用能够用一种便利而同步的方式向线程传递消息,NSObjec对象声明了在线程上执行selector的方法,这些方法异步地传递消息,而系统确保会同步地在目标线程上执行这些selector,每一个请求都会在目标线程的runloop上排上队,并按收到的顺序进行执行。

五. 线程间通讯

线程间通讯的表现为:

  • 一个线程传递数据给另外一个线程;
  • 在一个线程中执行完特定任务后,转到另外一个线程继续执行任务。

先看下官方文档推荐的线程通讯方案:

  1. 直接消息传递: 经过 performSelector 的一系列方法,能够实现由某一线程指定在另外的线程上执行任务。由于任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化
  2. 全局变量、共享内存块和对象: 在两个线程之间传递信息的另外一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,可是它们比直接消息传递更脆弱。必须使用锁或其余同步机制仔细保护共享变量,以确保代码的正确性。 不然可能会致使竞争情况,数据损坏或崩溃。
  3. 条件执行: 条件是一种同步工具,可用于控制线程什么时候执行代码的特定部分。您能够将条件视为关守,让线程仅在知足指定条件时运行。
  4. Runloop sources: 一个自定义的 Runloop source 配置可让一个线程上收到特定的应用程序消息。因为 Runloop source 是事件驱动的,所以在无事可作时,线程会自动进入睡眠状态,从而提升了线程的效率
  5. Ports and sockets:基于端口的通讯是在两个线程之间进行通讯的一种更为复杂的方法,但它也是一种很是可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其余进程和服务)进行通讯。为了提升效率,使用 Runloop source 来实现端口,所以当端口上没有数据等待时,线程将进入睡眠状态
  6. 消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,可是它们不如其余一些通讯技术高效
  7. Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通讯的高级实现。尽管能够将这种技术用于线程间通讯,可是强烈建议不要这样作,由于它会产生大量开销。分布式对象更适合与其余进程进行通讯,尽管在这些进程之间进行事务的开销也很高

我的经常使用的通讯方案有:

5.1 NSThread 线程间通讯

NSThread这套方案是通过苹果封装后,而且彻底面向对象的。不过它的生命周期仍是须要咱们手动管理,因此实际上使用也比较少。

  1. performSelectorOnMainThread
//数据请求完毕回调到主线程,更新UI资源信息  waitUntilDone  设置YES ,表明等待当前线程执行完毕
[self performSelectorOnMainThread:@selector(dothing:) withObject:@[@"1"] waitUntilDone:YES];
复制代码
  1. performSelectorInBackground
//将当前的逻辑转到后台线程去执行
[self performSelectorInBackground:@selector(dothing:) withObject:@[@"2"]];
复制代码
  1. 本身定义线程,将当前数据转移到指定的线程内去通讯操做
//支持自定义线程通讯执行相应的操做
NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(entryThreadPoint) object:nil];
[thread start];
//当咱们须要在特定的线程内去执行某一些数据的时候,咱们须要指定某一个线程操做
[self performSelector:@selector(dothing:) onThread:thread withObject:nil waitUntilDone:YES];
复制代码

5.2 GCD 线程间通讯

  1. 须要更新UI操做的时候使用下面这个GCD的block方法
//回到主线程更新UI操做
dispatch_async(dispatch_get_main_queue(), ^{
    //数据执行完毕回调到主线程操做UI更新数据
});
复制代码
  1. 有时候省去麻烦,咱们使用系统的全局队列:通常用这个处理遍历大数据查询操做
DISPATCH_QUEUE_PRIORITY_HIGH  全局队列高优先级
DISPATCH_QUEUE_PRIORITY_LOW 全局队列低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND  全局队里后台执行队列
// 全局并发队列执行处理大量逻辑时使用   
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

});
复制代码
  1. 当在开发中遇到一些数据须要单线程访问的时候,咱们能够采起同步线程的作法,来保证数据的正常执行
//当咱们须要执行一些数据安全操做写入的时候,须要同步操做,后面全部的任务要等待当前线程的执行
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    //同步线程操做能够保证数据的安全完整性
});
复制代码

5.3 NSOperation 线程间通讯

if ([[NSThread currentThread] isMainThread]) {
    NSLog(@"## 我是主线程 能够更新UI ##");
} else {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        NSLog(@"### 我是在主队列执行的block ####");
    }];
}
复制代码

六. 总结

本篇参照官方文档,学习了多线程的基础知识,下篇开始学习宏大的中央调度系统 - GCD。

参考资料

苹果官方文档 -- Threading Programming Guide

jackyshan -- iOS多线程详解:概念篇

YI_LIN -- 线程同步详解

我是好宝宝 -- iOS探索 多线程原理

相关文章
相关标签/搜索