【底层探索】- 多线程(一)


一、线程定义

1.1 基本概念

线程(Thread),有时被称为轻量级进程(Lightweight Progress, LWP),是程序执行流的最小单位。

一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。通常意义上,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级等资源(如打开文件和信号)。一个经典的线程与进程关系如图所示。

进程内的线程

归纳一下总结一下。

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程想要执行任务,必须得有线程,至少1条
  • 程序启动时,会默认开启一条线程,这条线程称为主线程或UI 线程

1.2 线程的访问权限

线程实际上也拥有自己的私有存储空间,包括一下方面

  • 线程局部存储(TLS,Thread Local Storage)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。
  • 寄存器。寄存器是执行流的基本数据,因此为线程私有。

从C 程序角度看,数据在线程之间是否私有如下所示:

二、进程的定义

进程时指在系统中正在进行的一个应用程序。每个进程之间时独立的内存空间和地址

面试题:

每个iOS App 有几个进程,为何这样设计?

iPhone 从早起1G 开始就显得比安卓系统更流畅,是因为它采用了单一进程 的策略。

这样避免了进程的来回切换,CPU 消耗大大降低;另外在数据安全性上,才用单一沙盒机制,隔离文件,保证App 的独立性,提高安全系数。

三、线程与进程关系

他们的关系主要从以下几个方面考虑

  • 地址空间:
    • 线程: 同一地址的线程共享本进程的地址空间
    • 进程:进程之间是独立的地址空间
  • 资源拥有
    • 线程:同一进程哪都线程共享本进程的资源,如内存空间、I/O、CPU 等
    • 进程:进程之间的资源上独立的
  • 健壮性——多进程比多线程健壮
    • 线程:进程内任意线程崩溃,会导致整个进程瘫痪。
    • 进程:进程崩扩后,在保护模式下不会对其他进程发生影响。
  • 资源消耗
    • 线程:进程切换时,消耗的资源大,效率高
    • 进程:线程无法切换,只能互相通信,消耗的资源较小
  • 执行过程
    • 线程:不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
    • 进程:每个独立的进程都有一个程序运行的入口、顺序执行序列和程序入口。
  • 基本属性。
    • 线程:是处理器调度的基本单位。
    • 进程:并不是。

四、多线程

4.1 多线程的原理

CPU 在单位时间片里快速在各个线程之间切换

不论是在多处理器的计算机上还是在单处理器的计算机上,线程总是”并发”执行的。当线程数量小于等于处理器数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器数量的情况,线程的并发会收到一些阻碍,因为此时至少有一个处理器会运行多个线程。

在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次进执行一小段时间(通常是几十到几百毫秒),这样每个线程就”看起来“在同事执行。

处于运行中线程拥有一段可以执行的时间,这段时间称为时间片(Time Slice)。当时间片用尽,该进程将进入就绪状态。

4.2 多线程的好处

  • 多线程能改进应用的响应机制
  • 多线程能改进多核系统下的应用的实时表现

4.3 线程的生命周期

多线程的生命周期,一般有以下几个

  • 新建
  • 运行 Running。此时线程正在执行
  • 就绪 Ready。此时线程可以立即运行,但是CPU 已经被占用
  • 堵塞 BLocked。此时CPU 正在执行sleep 方法/锁等占用动作,此时其他线程无法执行。
  • 死亡。任务完成,或者强制退出线程

4.4 线程池的原理

4.4.1 执行流程

  1. 判断线程池大小是否小于核心线程池等大小
    1. 如果否,继续判断工作队列是否已满
      1. 如果否,提交任务到工作队列
      2. 创建线程去执行任务
    2. 如归是,线程队列已满,判断线程池是否都已在工作
      1. 如果是,交给饱和策略
      2. 如果否,创建线程去执行任务
  2. 如果小与,此时可以直接创建线程去执行任务

4.4.2 策略

这里需要找一下 Oracle 关于线程池执行器的一些解释

类型 策略名
static class ThreadPoolExecutor.AbortPolicy
抛出RejectedExecutionException 的异常,并阻止系统运行
static class ThreadPoolExecutor.CallerRunsPolicy
拒绝任务,并在执行方法的调用线程里运行该拒绝任务,除非执行者被关闭,此时任务也会被弃用。
static class ThreadPoolExecutor.DiscardOldestPolicy
放弃执行最早的未完成请求,并尝试执行任务
static class ThreadPoolExecutor.DiscardPolicy
任务被拒绝添加时,线程池丢弃被拒绝的任务

4.5 线程和runloop 的关系

概念上,主要有以下几个

  1. runloop 与线程是一一对应的,一个runloop 对应一个核心进程(因为runloop 是可以嵌套的,但是核心的只有一个),他们的关系保存在一个全局的字典里。
  2. runloop 是来管理线程的,当线程的runloop 被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。
  3. runloop 在第一次获取时被创建,在线程时被销毁
  4. 对于主线程来说,runloop 在程序一启动就默认创建好了
  5. 对于子线程来说,runloop 是懒加载的,只有当我们使用的时候才会创建。所以在子线程用定时器要注意:确保子线程的runloop 被创建,不然定时器不会回调。

4.6 iOS 中的多线程

iOS 中一般有以下多线程

  • GCD

    就是我们最常使用的Grand Central Dispatch。底层由C 语言写成,由苹果公司开发的一种用于多核处理器的应用优化技术,初始使用在Mac OS X 10.6 以及移动端的iOS 4 上。GCD 的名字起源于位于美国纽约曼哈顿中城的大中央总站

    示例:

    - (IBAction)analyzeDocument:(NSButton *)sender {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSDictionary *stats = [myDoc analyze];
    dispatch_async(dispatch_get_main_queue(), ^{
    [myModel setDict:stats];
    [myStatsView setNeedsDisplay:YES];
    });
    });
    }
  • NSThread

    是苹果公司基于GCD 包装的一种面向对象的管理线程的方式,可以由程序员自行创建线程及退出。

  • pThread

    POSIX线程。POSIX 线程提供了一个基于C 语言的接口来创建线程。如果你不是在写Cocoa 应用,这恐怕是最理想的创建线程的选择。POSIX 接口相当简单也足够灵活的配置你的线程。

  • NSOperation

五、线程间的通讯

这个也是面试题经常会问到的,无论是中级还是高级的面试题。

苹果官方 线程相关的文档是这样介绍的,线程间的通讯如下:

一共有7种方式:

  • 直接传递消息:可以从一个线程往任意一个线程发送消息。这种能力意味着一个线程能从根本上在任意一个其他的线程执行某方法。因为他们被执行在目标线程的环境下,在该线程下消息被自动连续的发送。

    比较常见的就是performSelector

  • 全局变量:共享内存及共享对象,尽管共享变量快捷而渐变,但相比直接传递消息他们也更加不稳定。使用共享变量得用所活着其他同步机制格外小心的保护,以确保代码的准确性。一旦使用不当容易引起竞态条件、数据改变或者崩溃

  • Conditions:当一个线程执行了特定的部分代码,同步工具可以用来控制。你可以想象conditions 当作门卫,只有当状态条件满足的情况下,线程才允许执行。

  • Runloop 资源:自定义runloop 资源用来在线程里接受应用特定的信息。因为他们是事件驱动,runloop 资源会在无任务时将你的线程休眠,改进线程的效率。

  • 端口与套接字:基于端口的通讯是一种在两个线程通讯更精密的方式,也是一种可靠的技术。更重要的是,端口和套接字能用来与外部实体进行通讯,例如其他进程和服务。为了效率期间,端口通过使用runloop 资源来实现,因此等待端口无事可做时,线程也会被休眠。

  • 消息队列:经典的多处理器服务为来管理进出的数据,制定了一个先进先出队列(FIFO)概念。尽管消息队列简单而方便,他们也不像其他通讯技术那样高效。

  • 对象分发:对象分发是一种Cocoa 技术,提供了基于端口高级实现。尽管在线程内使用这门技术也能用,但鉴于使用它会带来的性能开支,高度不建议使用。分发对象更适合于进程间的通信,尤其是当进程消耗已经很高的情况下(虱子多了不咬,债多了不愁)。

六、线程的关闭

建议在创建线程的时候,就设计好响应取消和退出线程的消息。对于长任务来说,这可能意味着周期性的停止工作而查看是否有消息到达。如果确实有消息进来——要求线程退出,线程可能接下来可以实现一些必须的清理工作,然后优雅的退出;否则继续工作。

响应取消消息的方法是使用Runloop,看看下面的代码:

- (void)threadMainRoutine
{
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];

// Add the exitNow BOOL to the thread dictionary.
NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];

// Install an input source.
[self myInstallCustomInputSource];

while (moreWorkToDo && !exitNow)
{
// Do one chunk of a larger body of work here.
// Change the value of the moreWorkToDo Boolean when done.

// Run the run loop but timeout immediately if the input source isn't waiting to fire.
[runLoop runUntilDate:[NSDate date]];

// Check to see if an input source handler changed the exitNow value.
exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
}
}

文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【数据结构与算法】-(5)链表面试题解析 【数据结构与算法】-(5)链表面试题解析
【数据结构与算法】-(1)基础篇 【数据结构与算法】-(2)线性表基础 【数据结构与算法】-(3)循环链表(单向) 【数据结构与算法】-(4)双向链表和双向循环链表 【数据结构与算法】-(5)链表面试题解析 【数据结构与算法】
2020-04-09 李佳
下一篇 
【底层探索】- 多线程(二)GCD应用 【底层探索】- 多线程(二)GCD应用
一、面试题解析1.1 第一题A、提问:以下代码,会打印什么? dispatch_queue_t queue = dispatch_queue_create("lj", DISPATCH_QUEUE_SERIAL);NSL
2020-04-05 李佳
  目录