iOS多线程编程(一) 多线程基础

多线程系列篇章计划内容:
iOS多线程编程(一) 多线程基础
iOS多线程编程(二) Pthread
iOS多线程编程(三) NSThread
iOS多线程编程(四) GCD
iOS多线程编程(五) GCD的底层原理
iOS多线程编程(六) NSOperation
iOS多线程编程(七) 同步机制与锁
iOS多线程编程(八) RunLoop

现代计算机系统中,CPU作为计算机系统的运算控制核心,是信息处理、程序运行的最终执行单元。操作系统作为计算机的管理者,负责任务的调度资源的分配和管理,协调着各个硬件(如CPU、内存,硬盘、网卡等)有序的工作着。

在了解进程和线程之前,我们不妨先从操作系统开始。

一、操作系统

1.1 操作系统概念

操作系统是管理计算机硬件与软件资源计算机程序。作为计算机最基本也最为重要的”软件“系统,操作系统需要处理如管理与配置内存决定系统资源的供需的优先次序控制输入设备与输出设备操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。

[图片上传失败...(image-dd5998-1605604525529)]

从计算机用户的角度来说,计算机操作系统体现为其提供的各项服务;从程序员的角度来说,其主要是指用户登录的界面或者接口;如果从设计人员的角度来说,就是指各式各样模块和单元之间的联系。

1.2 操作系统发展的几个阶段

1.2.1 无操作系统阶段

最初的电脑没有操作系统,在这个阶段,计算机都是人工的去操作的,人们通过各种按钮来控制计算机,在操作的时候,每个用户都会独占这台计算机。并且计算机的CPU是等待人工去操作的,当用户进行输入和输出的时候,内存CPU都是空闲的。

因此,在没有操作系统的时代,计算机资源的利用率很低

1.2.2 批处理系统阶段

后来出现了汇编语言,操作人员通过有孔的纸带将程序输入电脑进行编译。
批处理系统阶段,计算机就无须再等待人工的输入了。因为这个时候用户会批量的导入任务,这样,用户在导入任务之后就可以离开,计算机就可以自动的去工作。

虽然在这个批处理系统里面,我们可以将任务批量的输入,但是这个时候计算机只能同时运行一个任务。也就是说,虽然任务是批处理输入的,但是计算机每次还是只能执行一个任务。

为了解决这一问题,在这个阶段提出了一个非常重要的概念,叫多道程序设计

多道程序设计

多道程序设计指的是在计算机内存中同时存放多个程序,并且这多个程序互相不干扰。 这里的多道程序在计算机的管理程序之下相互穿插运行,以此来提升计算机资源的利用率。

1.2.3 分时系统阶段

在这个阶段,最重要的设计就是人机交互的设计,在前面两个阶段,程序在执行的过程中,人是没有办法去干预的,在分时系统阶段,人机就可以进行交互了。并且,人可以实时的去调试程序,这个分时系统允许多个用户去共享这个计算机的资源,因此在这个阶段,计算的资源利用率大幅度提升。分时系统也是现在主流的系统。

1.3 操作系统的功能与意义

操作系统主要包括以下几个方面的功能 :

① 进程管理

其工作主要是进程调度,在单用户单任务的情况下,处理器仅为一个用户的一个任务所独占, 进程管理的工作十分简单。但在多道程序或多用户的情况下,组织多个作业或任务时,就要解决处理器的调度、分配和回收等问题。

② 存储管理

分为几种功能:存储分配、存储共享、存储保护 、存储扩张。

③ 设备管理

分有以下功能:设备分配、设备传输控制 、设备独立性。

④ 文件管理

文件存储空间的管理、目录管理 、文件操作管理、文件保护。

⑤ 作业管理

负责处理用户提交的任何要求。

计算机的操作系统对于计算机可以说是十分重要的。

  • 操作系统会统一管理着计算机资源
    比如我们需要计算机计算”1+1“,并不是直接告诉CPU说我们要算"1+1",而是借助操作系统,让操作系统去告诉硬件我们要做什么事情;
    再比如我们存储或读取一个文件的时候,我们不是直接去控制存储器里边的机械设备读取的,而是通过操作系统去读写这些信息的。
    操作系统实现了对计算机资源的抽象。这个抽象就是通过管理软件来实现的,这些管理软件屏蔽了硬件设备,并且给用户提供了逻辑设备,使得每个用户在使用的时候都是一样的。

  • 用户无需面向硬件接口编程
    计算机发展到现在,设备种类繁多复杂。操作系统提供统一的操作界面,屏蔽了不同设备之间的差异。有了操作系统我们就不需要关注不同的设备,也不需要关注不同的接口。
    也就是说我们无需面向存储器、IO设备这些硬件,而只需面向操作系统去编程就可以了。
    举个例子,在操作系统里边有IO设备管理软件,这个软件提供了读写接口,用户在进行编程时,直接调用这个接口,并不需要具体的接触某个IO设备。

  • 操作系统提供了用户与计算机之间的接口
    在不同的设备上,操作系统可向用户呈现多种操作手段。如:

图形窗口形式(比如在手机上,我们可以通过手指的触摸控制手机里的硬件设备(如摄像头、声音)、而在我们普通的PC端,我们主要是通过鼠标、键盘来控制硬件)。)

命令形式(Linux中通过在shell终端中输入命令的方式)

系统调用形式(主要是编程的时候,比如打开文件、读取数据这些操作,都是通过系统调用来完成的)
操作系统的简易性使得更多人能够使用计算机。更多的人可以使用计算机就意味着解放和发展的生产力,对人类科技的提升是大有帮助的。

1.4 小结

在没有操作系统的时代,资源只属于当前运行的程序,且计算机只能运行一个程序,计算机资源利用率很低。

有了多道程序设计的概念,为了实现程序的共用,以及对计算机资源的管理,便有了操作系统。

二、进程与线程

也是基于多道程序设计的概念,为了使多个程序能并发执行,以提高资源的利用率和系统的吞吐量。进程也就出现了,进程的作用就是合理的隔离资源(因为有多道程序的概念,所以在计算机中就可能会有多个进程共同的使用某一个物理设备,比如存储器。那么进程在这里面就是发挥运行资源隔离的作用)、提升资源利用率

一个程序在运行过程中会涉及很多操作,利用 CPU 计算、通过磁盘 IO 进行数据传输等等,我们知道当程序在进行磁盘 IO 的时候,因为速度问题,会比较慢,所在在这个过程中 CPU 会空闲下来,这会造成资源的浪费,正因为引入进程,在 A 进程进行磁盘 IO 的时候,会让出 CPU 给 B 进程,合理地利用了 CPU 资源,使得程序之间可以并发执行。

2.1 进程

进程(Process)是计算机中具有一定独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是系统进行资源分配和调度的基本单位(有了多道程序的概念,操作系统就可以对每个程序进行资源的分配)。

从狭义上,可以将进程理解为正在运行的程序的实例,当一个程序进入内存运行时,系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

下图为Mac系统下的活动监视器

[图片上传失败...(image-fd6c08-1605604525529)]

从图片来看,每一个进程都占有 CPU内存能耗磁盘网络等资源。

每一个进程都有它自己的地址空间,一般情况下,进程由 3 个部分组成,分别是程序代码数据集、栈进程控制块(Process Control Block)。

各自的作用如下:

  • 程序代码:描述进程要完成哪些功能以及如何完成。
  • 数据集、栈:程序在执行时所需要的数据和工作区。
  • 进程控制块(PCB):包含进程的描述信息和控制信息。用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

进程具有的特征:

  • 并发性:任何进程都可以同其他进行一起并发执行;
  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
  • 独立性:进程是系统进行资源分配和调度的一个独立单位;
  • 结构性:进程由程序,数据和进程控制块三部分组成

2.2 线程

在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位。任务调度采用的是时间片轮转抢占式调度方式,而进程作为任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

后来,随着计算机技术的发展,可运行的进程越来越多。进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)的出现,可以满足多个运行单位,而多个进程并行开销过大。因此出现了能独立运行的基本单位——线程(Threads)

线程是程序执行中一个单一顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量

2.3 进程与线程的关系:

一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程)。他们的关系如下:

  • 线程是依附于进程的,不能独立存在,它包含在进程之中,是进程中的实际运作单位。进程一旦结束,所有线程都结束。

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

  • 线程是进程中的一个执行单元,由CPU独立调度执行,负责当前进程中任务的执行。一个进程可以有一个或多个线程,线程会拥有自己的堆栈局部变量(不共享),但是它与同一进程中的多个线程将共享程序的内存空间,也就是该进程中的代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)等系统资源。

引用一个例子:

如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答,此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)

2.4 进程和线程的区别:

  • ①. 根本区别:进程是操作系统分配资源的最小单位,线程程序执行的最小单位。

所有与该进程有关的资源,都被记录在进程控制块(PCB)中。以表示该进程拥有这些资源或正在使用它们。进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
一个标准的线程由线程ID、当前指令指针PC、寄存器、堆栈线程控制表TCB组成。
而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

  • ②. 地址空间和其它资源: 进程之间相互独立,但同一进程下的各线程共享程序的内存空间及一些进程级的资源。某进程内的线程在其他进程不可见。

  • ③. 调度和切换:线程上下文切换比进程上下文切换要快得多。

  • ④. 通信机制:进程间通信,由于它们具有独立的数据空间,需要进程同步和互斥手段的辅助,以保证数据的一致性,方式不仅耗时,而且不方便;但同一进程下的线程之间数据共享,可以直接读写进程数据段(如全局变量)来进行通信;

  • ⑤. 内存分配:系统在运行的时候会为每个进程分配不同的内存空间,而线程除了CPU外,不会再分配空间,而是使用进程的资源空间。

2.5 举个例子:

image

三、多进程与多线程

现代操作系统,比如Mac OSUNIXLinuxWindows等,都是支持“多任务”的操作系统。

也就是操作系统可以同时运行多个任务。比如,你可以一边用浏览器上网,一边听音乐,一边写代码,对于操作系统而言,这就是多任务。

在早期单核CPU时代还没有线程的概念,只有进程。由于CPU执行代码都是顺序执行的。那么,单核CPU是怎么执行多任务的呢?

答案就是时间片轮转调度:简单地说就是把一个处理器划分为若干个短的时间片,每个进程会被操作系统分配一个时间片(即每次被 CPU 选中来执行当前进程所用的时间),每个时间片依次轮流地执行处理各个应用程序,时间一到,无论进程是否运行结束,操作系统都会强制将 CPU 这个资源转到另一个进程去执行,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果。

[图片上传失败...(image-35ce25-1605604525529)]

如上图所示,每一个小方格代表一个时间片,大约100ms。假设现在我同时开着 Word、QQ、网易云音乐三个软件,CPU 首先去处理 Word 进程,100ms时间一到,CPU 就会被强制切换到 QQ 进程,处理100ms后又切换到网易云音乐进程上,100ms后又去处理 Word 进程,如此往复不断地切换。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

随着运行的进程越来越多,人们发现进程的创建、撤销与切换存在着较大的时空开销,因此业界急需一种轻型的进程技术来减少开销。于是上世纪80年代出现了一种叫 SMP(Symmetrical Multi-Processing)的对称多处理技术,就是我们所知的线程概念。线程切换的开销要小很多,这是因为每个进程都有属于自己的一个完整虚拟地址空间,而线程隶属于某一个进程,与进程内的其他线程一起共享这片地址空间,基本上就可以利用进程所拥有的资源而无需调用新的资源,故对它的调度所付出的开销就会小很多。

[图片上传失败...(image-53a392-1605604525529)]

以 QQ 聊天软件为例,上文我们一直都在说不同进程如何流畅的运行,此刻我们只关注一个进程的运行情况。如果没有线程技术的出现,当 QQ 这个进程被 CPU “临幸”时,我是该处理聊天呢还是处理界面刷新呢?如果只处理聊天,那么界面就不会刷新,看起来就是界面卡死了。有了线程技术后,每次 CPU 执行100ms,其中30ms用于处理聊天,40ms用于处理传文件,剩余的30ms用于处理界面刷新,这样就可以使得各个组件可以“并行”的运行了。

自此之后,多线程技术得到广泛应用。

3.1 多进程 vs 多线程

多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。混合多进程和多线程的程序涉及到同步、数据共享的问题,这种模型更复杂,实际很少采用。

和多进程相比,多线程的优势在于:

  • 线程的调度与切换比进程很多,同时创建一个线程的开销也比进程要小很多;
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,线程间通信就是读写同一个变量,速度很快。而进程之间的通信需要以通信的方式(Inter Process Communication,IPC)进行。

多进程的优点在于:

  • 多进程程序更健壮,在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

3.2 多线程的意义:

多线程最大的意义,就是最大限度地利用CPU资源。

  • 某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待时间进行线程切换。如等待网络响应。
  • 某个操作可能会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会被中断。多线程可以让一个线程负责交互另一个线程负责计算
  • 程序本身要求并发操作,如一个多端下载软件(如Bittorrent)。
  • 多CPU或多核处理器,本身具备同时执行多个线程的能力,因此单线程程序无法全面发挥计算机的全部计算能力。
  • 多线程可以提高程序的效率。多线程同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。

当然,多线程也并不是百利而无害的。

  • 开启线程需要占用一定的内存空间(默认情况下,每条线程占512kb);如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
  • 线程越多,CPU在调用线程上的开销就越大;
  • 多程序编程的程序设计会更加复杂,如线程间的通信、多线程的数据共享等。

3.3 并行与并发:

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行;

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

四、 线程的状态(生命周期)

[图片上传失败...(image-533609-1605604525529)]

4.1 新建态(new)

创建一个Thread对象就生成一个新线程。创建完成后就需要为线程分配内存。当线程处于"新线程"状态时,仅仅是一个空线程对象,它还没有分配到系统资源。因此只能启动或终止它。任何其他操作都会引发异常。

例如,一个线程调用了new方法,并在调用start方法之前的就处于新线程状态,可以调用startstop方法。

4.2 就绪态(Runnable)

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,创建线程运行所必须的系统资源,并调度线程执行run()方法 ,当start()方法返回后,线程就处于就绪状态

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此,此时可能有多个线程处于就绪状态。

4.3 运行态(Running)

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。

4.4 阻塞态(Blocked)

所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞),可能将资源交给其它线程使用。

suspend()方法被调用。

sleep()方法被调用。

③ 线程使用wait()来等待条件变量。

④ 线程处于I/O请求的等待。

⑤ 线程试图得到一个,而该锁正被其他线程持有。

4.5 死亡态(Dead)

有两个原因会导致线程死亡:

run()方法正常退出而自然死亡;

② 发生异常或者被打断interrupt()导致线程终止。

run()方法返回,或别的线程调用stop()方法,线程进入死亡态。通常Apple使用它的stop()方法来终止它产生的所有线程。

文章参考:

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

进程知多少?

你可能感兴趣的:(iOS多线程编程(一) 多线程基础)