【Python之路】第九篇--Python基础之线程、进程和协程

时间:2023-03-08 17:26:38
【Python之路】第九篇--Python基础之线程、进程和协程

进程与线程的历史

  进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。

  我们编写的程序用来描述进程要完成哪些功能以及如何完成;

  数据集则是程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

  进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

  可以说,进程就是包括上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文。

  在早期的操作系统里,计算机只有一个核心,进程执行程序的最小单位,任务调度采用时间片轮转的抢占式方式进行进程调度。

  每个进程都有各自的一块独立的内存,保证进程彼此间的内存地址空间的隔离。

随着计算机技术的发展,进程出现了很多弊端:

  一、进程的创建、撤销和切换的开销比较大,

  二、由于对称多处理机(对称多处理机(SymmetricalMulti-Processing)又叫SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构)的出现,可以满足多个运行单位,而多进程并行开销过大。

也就是说:进程的颗粒度太大,每次都要有上下的调入,保存,调出。

如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。

程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。

这里的a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。

这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境的更为细小的CPU时间段。

  这个时候就引入了线程的概念。

  线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。

  线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。

  线程没有自己的系统资源,只拥有在运行时必不可少的资源。

  但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。

  一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

进程与线程之间的关系

  线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

  线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。

  线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。

  根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。

  没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  程序(program)只能有一个进程,一个进程就是一个程序。

  打开一个Chrome程序,会发现开了十几个进程,那就是十多个程序,操作系统给他们分配了彼此独立的内存,相互执行不受彼此约束,分配同样时间的CPU。

  对于用户而言,他们是一个整体,我们通常称之为应用程序(application)。

  对于计算机而言,一个进程就是一个程序,多个进程(比如一个浏览器的多个进程)对计算机而言就是多个不同的程序,它不会把它们理解为一个完整的“程序”。

  进程之间的关系只有父子关系,没有主从关系,他们之间是并行独立的。但是线程之间是有主从关系的,而且他们共享的是同一个内存块(包括程序、数据和堆栈)。

  父进程 fork 子进程,这个子进程会拷贝一份内存块,把程序和数据都复制过去。

  子进程之后就完全独立了,父进程与子进程之间的关系,与其他进程的关系都是一样的,平等的,谁也管不着谁了,他们也只能采用进程间通信才能相互了解。

  父进程over了,子进程可以照样活的好好的。

  同时,进程可以由多个线程组成,这称之为多线程程序,

  子线程由主线程派生,而依附于主线程。主线程一旦over,进程就over了,其他子线程更是over了。

  他们的内存和数据都是同一份,没有进行隔离(既方便,也危险),不需要额外的通信函数。

  一个计算机可以有多个进程,这称之为多任务,他们共享的是CPU,硬盘,打印机,显示器,但他们的内存是独立的,所以需要进程间通信,这是计算机发展的第一步。

  一个进程可以有多个线程,这称之为多线程,他们除了共享进程间的共享内容之外,还共享内存,这是计算机发展的第二步,主要是为了满足并行运算时共享数据,无需额外的通信。

  结论是:一个程序(program)就是一个正在执行的进程,而每个进程,可以是单线程的,也可以是多线程的。一个应用程序(application)通常由多个程序组成。

并发、并行、隔离

并发

  指一个系统具有处理多个任务的能力(cpu切换,多道技术)

  是为了尽量让硬件利用率高,线程是为了在系统层面做到并发。线程上下文切换效率比进程上下文切换会高很多,这样可以提高并发效率。

并行

  指一个系统具有同时处理多个任务的能力(cpu同时处理多个任务)

  并行是并发的一种情况,子集

隔离

  也是并发之后要解决的重要问题,计算机的资源一般是共享的,隔离要能保障崩溃了这些资源能够被回收,不影响其他代码的使用。

  所以说一个操作系统只有线程没有进程也是可以的,只是这样的系统会经常崩溃而已,操作系统刚开始发展的时候和这种情形很像。

所以:

  线程和并发有关系,进程和隔离有关系。

  线程基本是为了代码并发执行引入的概念,因为要分配cpu时间片,暂停后再恢复要能够继续和没暂停一样继续执行;

  进程相当于一堆线程加上线程执行过程中申请的资源,一旦挂了,这些资源都要能回收,不影响其他程序。

那为什么python在多线程中为什么不能实现真正的并行操作呢?

就是在多cpu中执行不同的线程(我们知道JAVA中多个线程可以在不同的cpu中,实现并行运行)

这就要提到python中大名鼎鼎GIL,那什么是GIL?

GIL(全局解释器锁):

  无论你启多少个线程,你有多少个cpu,Python在执行的时候只会的在同一时刻只允许一个线程(线程之间有竞争)拿到GIL在一个cpu上运行。

  当线程遇到IO等待或到达者轮询时间的时候,cpu会做切换,把cpu的时间片让给其他线程执行,cpu切换需要消耗时间和资源,

  所以计算密集型的功能(比如加减乘除)不适合多线程,因为cpu线程切换太多,IO密集型比较适合多线程。

Python GIL(Global Interpreter Lock) 

  In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) 

  上面的核心意思就是,无论你启多少个线程,你有多少个cpu,Python在执行的时候会淡定的在同一时刻只允许一个线程运行。

  首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

  就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。

  有名的编译器例如GCC,INTEL C++,Visual C++等。

  Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。

  像其中的JPython就没有GIL。

  然而因为CPython是大部分环境下默认的Python执行环境。

  所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。

  所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

  这篇文章透彻的剖析了GIL对python多线程的影响,强烈推荐看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf

threading模块

  Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。