iOS概念攻坚之路(四):多线程

前言

咱们如今所使用的操做系统模式是 多任务(Multi-tasking)系统,操做系统接管全部的硬件资源,并且自己运行在一个受硬件保护的级别。全部的应用程序都是以 进程(Progress) 的方式运行在比操做系统权限更低的级别。每一个进程都有本身独立的地址空间,使得进程之间的地址空间相互隔离。CPU 由操做系通通一进行分配,每一个进程根据进程优先级的高低都有机会获得 CPU,可是,若是运行时间超过了必定的事件,操做系统会暂停该进程,将 CPU 资源分配给其余等待运行的进程。这种 CPU 的分配方式即所谓的 抢占式(Preemptive),操做系统能够强制剥夺 CPU 资源而且分配给它认为目前最须要的进程,若是操做系统分配给每一个进程的时间都很短,即 CPU 在多个进程间快速的切换,从而形成了不少进程在同时运行的假象。目前几乎全部现代的操做系统都是采用这种方式。html

计算机发展早期,一个 CPU 只能运行一个程序,当执行 I/O 操做,好比读取磁盘数据时,CPU 就会处于空闲状态,这显然是一种浪费。后来人们迅速发明了 多道程序(Multiprogramming),当某个程序无需使用 CPU 时,监控程序就把另外的正在等待 CPU 资源的程序启动,不过它没有一个优先级的概念,就算某些任务急需 CPU,也极可能须要等待很长的时间。通过改进后,人们又发明了 分时系统(Time-Share System),每一个程序运行一段时间后都主动让出 CPU 给其余程序,使得一段时间内每一个程序都有机会运行一小段时间。不过这种系统的问题在于,若是一个程序在进行一个很耗时的操做,一直霸占 CPU,那么操做系统也是没有办法的,好比一个程序进入了一个 while(1) 的死循环,那么整个系统都会中止。再进一步的发展,就是咱们上面提到的多任务系统了。ios

如此发展的目的是,尽量最大限度的利用 CPU,到这里,出现了一个进程的概念,而线程与进程,有着脱不开的关系。程序员

什么是进程

维基百科:算法

进程(Process),是计算机中 已运行程序 的实体。进程曾经是分时系统的基本运做单位。在面向进程设计的系统(如早期的 UNIX,Linux2.4 及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操做系统,Linux 2.6 及更新版本)中,进程自己不是基本运行单位,而是 线程的容器。程序自己只是 指令,数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实现。安全

用户下达运行程序的命令后,就会产生进程。同一程序可产生多个进程(一对多关系),以容许同时有多位用户运行同一程序,却不会相互冲突。网络

进程须要一些资源才能完成工做,如 CPU 使用时间、存储器、文件及 I/O 设备,且为依序逐一进行,也就是每一个 CPU 核心任什么时候间内仅能运行一项进程。多线程

百度百科:并发

进程(Process) 是计算机中的 程序关于某数据集合上的一次运行活动,是系统进行 资源分配 和调度的基本单位,是操做系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。异步

进程是一个具备独立功能的程序关于某数据集合的一次运行活动。它能够申请和拥有系统资源,是一个 动态 的概念,是一个活动的实体。它不仅是程序的代码,还包括当前的活动,经过程序计数器的值和处理寄存器的内容来表示。函数

进程的概念主要有两点:第一,进程是一个实体。每个进程都有它本身的地址空间,通常状况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动进程调用的指令和本地变量。第二,进程是一个「执行中的程序」。程序是一个没有生命的实体,只有处理器赋予程序生命时(操做系统执行之),它才能成为一个活动的实体,咱们称其为进程。

因此,进程是程序的一个实体,是执行中的程序,而程序是指令、数据及其组织形式的描述。程序不能单独执行,只有将程序加载到内存中,系统为它分配资源后才可以执行,这种 执行的程序 称之为进程。因此进程是一个动态的概念,与程序的区别在于,程序是指令的集合,是进程运行的静态描述文本,而进程则是程序在系统上执行的动态活动。

能够这么理解,咱们写的 APP,是一个程序,咱们装到手机上,此时它还不是进程,当咱们点击它,系统为它分配资源,运行,此时它能够被称为进程。

另外二者都提到,在现代的面向程序设计的计算机结构中,进程是线程的容器。在现在的操做系统中,线程才是最小的调度单位,而进程,是资源分配的最小单位,一个进程包含一个或多个线程。

来看看进程的内容:

  • 那个程序的可执行机器代码的一个在存储器的映象。
  • 分配到别的存储器(一般是一个虚拟的一个存储器区域)。存储器的内容包括可执行代码、特定于进程的数据(输入、输出)、调用堆栈、堆栈(用于保存运行时运输中途产生的数据)。
  • 分配给该进程的资源和操做系统描述符,诸如文件描述符(UNIX术语)或文件句柄(Windows)、数据源和数据终端。
  • 安全特性,诸如进程拥有者和进程的权限集(能够允许的操做)。
  • 处理器状态(中文),诸如寄存器内容、物理存储器定址等。当进程正在运行时,状态一般存储在寄存器,其余状况在存储器。

什么是线程

内核线程、轻量级进程、用户线程

前面提到,进程是线程的容器,而在现代的多数操做系统中,线程是调度的最小单位。咱们来看看线程的定义:

维基百科:

线程(Thread) 是操做系统可以进行运算调度的最小单位。它被包含在进程之中,是进程中的 实际运做单位。一条线程指的是 进程中一个单一顺序的控制流,一个进程中能够并发多个线程,每条线程并行执行不一样的任务。在 Unix System V 及 SunOS 中也被称为轻量级进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

这边提到了几个概念:内核线程,轻量级进程,用户线程,咱们分别来看看它们的具体概念:

内核线程

内核线程就是内核的分身,一个分身能够处理一件特定事情。这在处理异步事件如异步 IO 时特别有用。内核线程的使用是廉价的,惟一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫作多线程内核(Multi-Threads kernel)。

内核线程只运行在内核态,不受用户态上下文的拖累。

轻量级进程

轻量级进程(LWP)是一种由 内核支持的用户线程。它是基于内核线程的高度抽象,所以只有先支持内核线程,才能有 LWP。每个进程有一个或多个 LWP,每一个 LWP 由一个内核线程支持。这种模型被称为一对一模型。在这种实现的操做系统中,LWP 就是用户线程。

因为每一个 LWP 都与一个特定的内核线程相关联,所以每一个 LWP 都是一个独立的线程调度单元。即便有一个 LWP 在系统调用中阻塞,也不会影响整个进程的执行。

轻量级进程的局限性:

  • 大多数 LWP 的操做,如创建、析构以及同步,都须要进行系统调用,系统调用的代价相对较高(须要在用户态和内核态中切换)
  • 每一个 LWP 都须要有一个内核线程支持,所以 LWP 要消耗内核资源(内核线程的栈空间)。所以一个系统不能支持大量的 LWP。(图片的 P 指进程)

将之称之为轻量级进程的缘由多是:在内核线程的支持下,LWP 是独立的调度单元,就像普通的进程同样。因此 LWP 的最大特色仍是每一个 LWP 都有一个内核线程支持。

用户线程

LWP 虽然本质上属于用户线程,但 LWP 线程库是创建在内核之上的,LWP 的许多操做都要进行系统调用,所以效率不高。而这里的用户线程指的是 彻底创建在用户空间的线程库,用户线程的创建、同步、销毁、调度彻底在用户空间完成,不须要内核的帮助,所以这种线程的操做是及其快速且低消耗的。

上图是最初的一个用户线程模型,从中能够看出,进程中包含线程,用户线程在用户空间中实现,内核并无直接对用户线程进行调度,内核的调度对象和传统进程同样,仍是进程自己,内核并不知道用户线程的存在。用户线程之间的调度由在用户控件的线程库实现。

这是多对一模型,其缺点在于一个用户线程若是阻塞在系统调用中,则整个进程都将会阻塞。

增强版的用户线程 —— 用户线程+LWP

这种模型是所谓的多对多模型。用户线程库仍是彻底创建在用户空间中,所以用户线程的操做仍是很廉价,所以能够创建任意多须要的用户线程。操做系统提供了 LWP 做为用户线程和内核线程之间的桥梁。LWP 仍是和前面提到的同样,具备内核线程支持,是内核的调度单元,而且用户线程的系统调用要经过 LWP,所以进程中某个用户线程的阻塞不会影响整个进程的执行。用户线程库将创建的用户线程关联到 LWP 上,LWP 与用户线程的数量不必定一致。当内核调度到某个 LWP 上时,此时与该 LWP 关联的用户线程就被执行。

不少文献中认为轻量级进程就是线程,实际上这种说法并不彻底正确,只有在用户线程彻底由轻量级进程构成时,才能够说轻量级进程就是线程。

更多有关内核线程、轻量级进程、用户线程三种线程的概念,能够看看 这篇文章

在现代操做系统中,再也不将进程当作操做的基本单元了,而是把线程当作基本单元,一个进程中能够存在多个线程。一个进程内的全部线程都共享虚拟内存空间。进程这个概念依然以一个或多个线程的 容器 的形式保存下来。进程每每都是多线程的,当一个进程只是单线程时,进程和线程两个属于能够互换使用。

线程的结构、访问权限

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

线程能够访问进程内存里的全部数据,甚至包括其余线程的堆栈(若是它知道其余线程的堆栈地址,不过这种状况不多见),在实际运用中,线程也拥有本身的私有存储空间,包括如下方面:

  • 栈(尽管并不是彻底没法被其余线程访问,但通常状况下仍然能够认为是私有的资源)
  • TLS(Thread Local Storage,线程局部存储)。TLS 是某些操做系统为线程单独提供的私有空间,一般只具备颇有限的容量
  • 寄存器(包括 PC 寄存器),寄存器是执行流的基本数据,所以为线程私有

线程与进程的数据是否私有以下表:

线程私有 进程之间共享(进程全部)
局部变量 全局变量
函数的参数 堆上的数据
TLS 函数的静态变量
程序代码,任何线程都有权利读取并执行任何代码
打开的文件,A 线程打开的文件能够由 B 线程读写

线程调度与优先级

无论是多处理器仍是单处理器,咱们看到的线程彷佛老是 “并发” 执行的,实际状况是,只有当线程数量小于或等于处理器的数量时(而且操做系统支持多处理器),线程的并发才是真正的并发(也就是并行),不一样的线程运行在不一样的处理器上,彼此之间互不相干。对于线程数量大于处理器数量的状况,至少有一个处理器会运行多个线程,此时的并发只是一种模拟出来的状态:操做系统会让这些多线程程序轮流执行,每次仅执行以小段时间(一般是几十到几百毫秒),这样每一个线程 “看起来” 在同时执行。

这样的一个不断在处理器上切换不一样线程的行为称之为 线程调度(Thread Schedule),在线程调度中,线程一般拥有至少三种状态,分别是:

  • 运行(Running):此时线程正在执行
  • 就绪(Ready):此时线程能够马上执行,但 CPU 已经被占用
  • 等待(Waiting):此时线程正在等待某一事件(一般是 I/O 或同步)发生,没法执行

来看一下线程的生命周期:

处于运行中线程拥有一段能够执行的时间,这段时间称为时间片(Time Slice),当时间片用尽的时候,该线程进入就绪状态。若是在时间片用尽以前线程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度系统就会选择一个其余的就绪线程继续执行。在一个处于等待状态的线程所等待的事件发生以后,该线程将进入就绪状态。

线程调度自多任务操做系统问世以来就不断被提出不一样的方案和算法。如今主流的调度方式尽管各不相同,但都带有 优先级调度(Priority Schedule)轮转法(Round Robin) 的痕迹。所谓轮转法,便是以前提到的让各个线程轮流执行一小段时间的方法,这决定了线程之间是交错运行的。而优先级调度则决定了线程按照什么顺序轮流执行。在具备优先级调度的系统中,线程都拥有各自的 线程优先级(Thread Priority)。具备高优先级的线程会更早的执行,而低优先级的线程经常要等到系统中已经没有高优先级的可执行的线程存在时才可以执行。

系统会根据不一样线程的表现自动调整优先级,以使得调度更有效率。例如一般状况下,频繁的进入等待状态(进入等待状态,会放弃以后仍然课占用的时间份额,也就是咱们说的线程休眠,不会占用 CPU 资源)的线程(例如处理 I/O 的线程)比频繁进行大量计算,以致于每次都要把时间片所有用尽的线程要受欢迎的多。通常把频繁等待的线程称之为 IO 密集型线程(IO Bound Thread),而把不多等待的线程称为 CPU 密集型线程(CPU Bound Thread)。IO 密集型线程老是比 CPU 密集型线程容易获得优先级的提高。

在优先级调度下,存在一种 饿死(Starvation) 的现象,一个线程被饿死,是说它的优先级较低,在它执行以前,老是有高优先级的线程要执行,所以这个低优先级线程始终没法执行。当一个 CPU 密集型线程得到较高的优先级时,许多低优先级的线程就极可能饿死。而一个高优先级的 IO 密集型线程因为大部分时间都处于等待状态,所以相对不容易形成其余线程饿死。为了不饿死现象,调度系统经常会逐步提高那些等待了过长时间的得不到执行的线程的优先级。在这样的手段下,一个线程只要等待足够多的事件,其优先级必定会提升到足够让它执行的程度。

总结一下,在优先级调度的环境下,线程的优先级改变通常有三种方式:

  • 用户指定优先级
  • 根据进入等待状态的频繁程度提高或下降优先级
  • 长时间得不到执行而被提高优先级

线程安全

多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时均可能被其余的线程改变,所以多线程程序在并发时数据的一致性变得很是重要。

为了不多个线程同时读写同一个数据而产生不可预料的后果,咱们要将各个线程对同一个数据访问 同步(Synchronization)。所谓同步,即指在一个线程访问数据未结束的时候,其余线程不得对同一个数据进行访问。如此,对数据的访问被原子化了。

原子操做:单指令的操做,不管如何,单条指令的执行都不会被打断。

同步的最多见方法是使用 锁(Lock)。锁是一种非强制机制,每个线程在访问数据或资源以前首先试图 获取(Acquire) 锁,并在访问结束以后 释放(Release) 锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁从新可用。

二元信号量(Binary Semaphore) 是最简单的锁,它只有两种状态:占用与非占用。它适合只能被惟一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会得到该锁,并将二元信号量置为占用状态,此后其余的全部试图获取该二元信号量的线程将会等待,直到该锁被释放。

对于容许多个线程并发访问的资源,多元信号量简称 信号量(Semaphore),它是一个很好的选择。一个初始值为 N 的信号量容许 N 个线程并发访问。线程访问资源的时候首先获取信号量,进行以下操做:

  1. 将信号量的值减 1
  2. 若是信号量的值小于 0,则进入等待状态,不然继续执行

访问完资源以后,线程释放信号量,进行以下操做:

  1. 将信号量的值加 1
  2. 若是信号量的值小于 1,唤醒一个等待中的线程

互斥量(Mutex) 和二元信号量很相似,资源仅同时容许一个线程访问,但和信号量不一样的是,信号量在整个系统能够被任意线程获取并释放,也就是说,同一个信号量能够被系统中的一个线程获取以后由另外一个线程释放。而互斥量则要求哪一个线程获取了互斥量,哪一个线程就要负责释放这个锁,其余线程去释放互斥量是无效的。

临界区(Cirtical Section) 是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程建立了一个互斥量或信号量,另外一个进程试图去获取该锁是合法的。然而,临界区的做用范围仅限于本进程,其余的进程没法获取该锁。除此以外。临界区具备和互斥量相同的性质。

读写锁(Read-Write Lock) 致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取老是没有问题的,但假设操做都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步的手段来避免出错。若是咱们使用上述信号量、互斥量或临界区中的任何一种来进行同步,尽管能够保证程序正确,但对于读取频繁,而仅仅偶尔写入的状况,会显得很是低效。读写锁能够避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的(Shared)独占的(Exclusive)。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。若是锁处于共享状态,其余线程以共享的方式获取锁仍然会成功,此时这个锁被分配给了多个线程。然而,若是其余线程试图以独占的方式获取已经处于共享状态的锁,那么它将必须等待锁被全部的线程释放。相应的,处于独占状态的锁将阻止任何其余线程获取该锁,不论它们试图以哪一种方式获取。读写锁的行为能够总结为下表:

读写锁状态 以共享方式获取 以独占方式获取
自由 成功 成功
共享 成功 等待
独占 等待 等待

条件变量(Condition Variable) 做为一种同步手段,做用相似于一个栅栏。对于条件变量,线程能够有两种操做,首先线程能够等待条件变量,一个条件变量能够被多个线程等待。其次,线程能够唤醒条件变量,此时某个或全部等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可让许多线程一块儿等待某个事件的发生,当事件发生时(条件变量被唤醒),全部的线程能够一块儿恢复执行。

锁与线程同步,会单独再开一篇文章来讲,锁的概念都是同样的,只是实现的手段不同。

线程和进程的由来

我在看 进程和线程的一个简单解释 这篇文章的时候,看到一个回答,用来讲明进程和线程的由来比较合适:

  1. 在单核计算机里,有一个资源是没法被多个应用程序并行使用的:CPU。

没有操做系统的状况下,一个程序一直独占着所有 CPU。

若是要有两个任务来共享一个 CPU,程序员就须要仔细地为程序安排好运行计划 —— 某时刻 CPU 由程序 A 来独享,下一时刻 CPU 由程序 B 来独享。

而这种安排计划后来成为 OS 的核心组件,被单独命名为 Scheduler(调度器)。它关心的只是怎样把单个 CPU 的运行拆分红一段一段的 “运行片”,轮流分给不一样的程序去使用,而在宏观上,由于分配切换的速度极快,就制造出多线程并行在一个 CPU 上的假象。

  1. 在单核计算机里,有一个资源能够被多个程序共用,然而会引出麻烦:内存。

在一个只有调度器,没有内存管理组件的操做系统上,程序员须要手工为每一个程序安排运行的空间 —— 程序 A 使用武力地址 0x00-0xff,程序 B 使用物理地址 0x100-0x1ff,等等。

然而这样作有个很大的问题:每一个程序都要协调商量好怎样使用同一个内存上的不一样空间,软件系统和硬件系统千差万别,使这种定制方案没有可行性。

为了解决这个麻烦,计算机引入了「虚拟内存」的概念,从三方面入手来作:

  • 硬件上,CPU 增长了一个专门的模块叫 MMU,负责转换虚拟地址和物理地址
  • 操做系统上,操做系统增长了另外一个核心组件:「Memory Management」,即内存管理模块,它管理物理内存、虚拟内存相关的一系列事务。
  • 应用程序上,发明了一个叫作「进程」的模型,每一个进程都用 彻底同样 的虚拟地址空间,而后经由操做系统和硬件 MMU 协做,映射到不一样的物理地址空间上。不一样的「进程」都有各自独立的物理内存空间,不用一些特殊手段,是没法访问别的进程的物理内存的。
  1. 如今,不一样的应用程序,能够不关心底层的物理内存分配,也不关心 CPU 的协调共享了。然而还有一个问题存在:有一些程序,想要共享 CPU,而且还要共享一样的物理内存,这时候,一个叫「线程」的模型就出现了,它们被包裹在进程里面,在调度器的管理下共享 CPU,拥有一样的虚拟空间地址,同时也共享同一个物理地址空间,然而,它们没法越过包裹本身的进程,去访问另一个进程的物理地址空间。

为何要使用多线程

  • 某个操做可能会陷入长时间等待,等待的线程会进入睡眠状态,没法继续执行。多线程执行能够有效利用等待的时间,典型的例子就是等待网络响应。
  • 某个操做(经常是计算)会消耗大量的时间,若是只有一个线程,程序和用户之间的交互会中断。多线程可让一个线程负责交互,另外一个线程负责计算。
  • 程序逻辑自己就要求并发操做,例如一个多端下载软件。
  • 多 CPU 或多核计算机,自己就具备同时执行多个线程的能力,所以单线程程序没法全面的发挥计算机的所有计算能力。
  • 相对于多进程引用,多线程在数据共享方面效率要高不少。

总结

其实咱们最主要是两个问题:什么是进程和什么是线程。

什么是进程?

进程是计算机中已运行程序的主体,在以前的分时系统中,是系统的基本运做单位。不过在现在的面向线程设计的系统中,进程是线程的容器。它的概念主要有两点:第一,进程是一个实体,每个进程都有它本身的地址空间。第二,进程是一个执行中的程序,程序是一个没有生命的实体,只有处理器赋予程序生命(如点击运行),它才能成为一个活动的实体,也就是进程。

进程是系统进行资源分配的最小单位。

什么是线程?

线程,有时候被称为轻量级进程,它包含在进程之中,是进程中的实际运做单位,一条线程指的是进程中一个单一顺序的执行流。线程共享进程的全部数据而且拥有本身私有的存储空间。线程又份内核线程和用户线程。

线程是系统进行调度的最小单位。

当咱们要运行一个程序,系统为咱们分配资源,而后运行,此时称为进程,然而真正运行的不是进程,而是进程内的某个执行流,也就是线程,一个进程至少有一条线程。

另外还有线程相关的一些知识点,好比线程的访问权限、结构、生命周期、安全等。

关于线程和进程的话题经久不息,若是文中有理解错误的地方,欢迎你们指出。

参考文章

iOS 多线程全套

内核线程、轻量级进程、用户线程三种线程概念解惑(线程≠轻量级进程)

进程和线程的一个简单解释

相关文章
相关标签/搜索