python多线程协程并行并发概念

线程与进程

进程

我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序侧是具有某种功能的程序,程序是运行于操作系统之上的。 (为了缓解头脑胀痛, 斜体字大体过一遍即可)
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

线程

在早期的操作系统中并没有线程的概念,在当时进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。进程间的通信与同步完全袭来操作系统内核转发与支持。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位 (这句话一定要理解);
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多。

线程与进程关系的示意图:


 
线程与进程关系.png

某个进程中单线程与多线程的关系


 
单线程与多线程.png

从别处看到线程是进程的一个实体,是程序执行的最小单位(线程也被称为轻量级进程).
按照我的理解:QQ音乐正在运行,QQ音乐就是个进程,而这个进程中有多个线程在跑着(各有各的任务);我们平常编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。
如果我们要同时执行多个任务怎么办:一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务;还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务(之后详细讲)。

线程与cpu核心数的关系

现在都是多核的处理器,那么在多核处理器的情况下,线程是怎样执行呢?这就需要了解内核线程。

多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器\性能”可以看到4个CPU的监视器,四核八线程可以看到8个CPU的监视器。

上面的应该可以理解,但是有个注意点:程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程. 那为什么要说出上面那段文字呢? 我心中的困惑是:一般我们买的CPU包装盒上写的 四核八线程的"线程" 到底是什么线程?跟系统的线程是一个东东么?上面说得很清楚了,内核线程与轻量级进程(即线程)的区别,即线程.

由于每个轻量级进程都需要一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
下图中:K代表内核线程,LWP代表轻量级进程,U代表 用户线程

 
内核线程用户线程.jpg

 

问题一:什么是用户线程?

用户线程是完全建立在用户空间的线程库(比如python的threading库),用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。

问题二:请简单解释下图中流程?

还是上面的例子,当你运行一个python脚本时,等于创建可一个进程,光有进程没卵用,必须有程序执行的最小单位嘛,所以同时系统会创建一个LWP,程序就靠它运行,因为是第一个线程,也称它为主线程。然后,脚本跑着跑着,需要创建新的线程运行其他任务时,主线程程序安排一个”创建线程“的口号(比如调用一个线程库)来调用产生U。

问题二:系统中有时会有那CPU核心数的数量影响着什么嘛?

(不是很确定),多核就是系统同时可以运行多个线程,比如双核可以同时执行两个线程。单核只能一次执行一个线程。
如果答案真如上面那样:那为什么能有百来个多线程同时存在?那么就可以引申到下面的话题了。

并发和并行

并行(parallelism)

这个概念很好理解。所谓并行,就是同时执行的意思,无需过度解读(在日本跟台湾,翻译成平行,这就更好理解了)。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。所以,单线程永远无法达到并行状态。
要达到并行状态,最简单的就是利用多线程和多进程。但是 Python 的多线程由于存在著名的 GIL,无法让两个线程真正“同时运行”,所以实际上是无法到达并行状态的。

那么对于上面的问题三,我们是否可以认为,一个四核八线程的CPU,最多允许八个线程同时执行?某个时间点就只有八个,然后下一时间,其他八个再执行=.=(不展开。有点晕)

在并行性开发时,不但可以考虑多进程并行执行,而且还可以开发出进程内多线程并行的程序,如下图。


 
并行执行实现方法.jpg

并发(concurrency)

要理解“并发”这个概念,必须得清楚,并发指的是程序的“结构”。当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计
正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。

这句话的重点有两个。我们先看“(操作)在重叠的时间段内进行”这个概念。它是否就是我们前面说到的并行呢?是,也不是。并行,当然是在重叠的时间段内执行,但是另外一种执行模式,也属于在重叠时间段内进行。这就是协程(或者简单点多线程也可以,然后把task1,task2改成线程1,线程2)。
使用协程时,程序的执行看起来往往是这个样子:

 
coroutine.jpg

 

task1, task2 是两段不同的代码,比如两个函数,其中黑色块代表某段代码正在执行。注意,这里从始至终,在任何一个时间点上都只有一段代码在执行,但是,由于 task1 和 task2 在重叠的时间段内执行,所以这是一个支持并发的设计。与并行不同,单核单线程能支持并发(特指协程)。

那么,如何实现支持并发的设计?两个字:拆分。
之所以并发设计往往需要把流程拆开,是因为如果不拆分也就不可能在同一时间段进行多个任务了。这种拆分可以是平行的拆分,比如抽象成同类的任务,也可以是不平行的,比如分为多个步骤。

并发和并行的区别
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计

同步和异步理解

这里我就简单举个例子就好了:web网页,点一下发送邮件按钮,然后发送(发送过程5秒),发送成功之网页显示跳转成功。
同步:点了发送之后,慢慢等五秒,然后显示成功
异步:点了发送之后,直接显示成功,后台默默的还在发送,直到发送完毕。
关于阻塞啥的我这里就不提了,已经有些晕了。毕竟在python中有些集成的库已经将这些问题考虑进去了,不需要重复造轮子,有兴趣的可以自己去查一查。

python的相关模块

  • threading 多线程库
  • concurrent.futures中的ThreadpoolExecutor 线程池
  • concurrent.futures中的ProcesspoolExecutor 进程池
  • subprocess 子进程
  • yield关键字 协程
  • asyncawait(python3.5新加入) 协程库

其他我想要说的几个点

  1. 在多线程这方面我代码中用的比较少.有时偶尔用到,我会直接选用ThreadpoolExecutor类。这个接口抽象层级很高,多线程直接使用,无需关心任何实现细节,啥线程死锁等问题都不需要担心。

  2. 多线程在单cpu中其实也是顺序执行的,不过系统可以帮你切换那个执行而已,其实并没有快(反而慢)
    ;多个cpu的话就可以在两个cpu中同时执行了。

  3. 接上面那一条,虽然python有GIL锁,启用多线程的时候只能用一个cpu核心,但python线程仍然适合I/O密集型应用:标准库中每个使用 C 语言编写的 I/O 函数都会释放 GIL,因此,当某个线程在等待 I/O 时, Python 调度程序会切换到另一个线程。 (意思就是说I/O操作的延迟大于过程中程序运行时间,我完全可以在延迟的时候,切换到其他线程,做其他操作,如下图情况C)


     
    单线程多线程.jpg
  4. 多线程与协程的区别:协程执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,不像多线程那样要切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。




链接:https://www.jianshu.com/p/48893a7125a1

posted on 2020-04-07 21:09  不要挡着我晒太阳  阅读(1472)  评论(0编辑  收藏  举报

导航