《并发哲学:从编程入道到开悟升天》4.3 谈并发与进程、线程、协程

谈并发与进程、线程、协程

作为操作系统内重要的抽象概念,各类语言在谈起底层资源调度的时候,往往离不开探讨进程和线程。诸如进程和线程的区别,也往往是各种面试过程中常常考察的问题。本节我们从并发视角出发,来重新审视一下进程、线程,和由Golang等语言引入进来独有的协程的相关知识。

什么是进程、线程、协程

这里我就直接引用网上来源的普遍说法了。

1、进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

2、线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少。

3、协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

一般说来,在具体的面试过程中,如果你能把加粗部分熟记,并自信地介绍给面试官的话,面试官一般会认为你对这些基础知识有一个基本的掌握。简而言之,上文中加粗部分恰恰是每个概念内最核心的部分,也是进程、线程、协程各自的特色。

那么,这些特色究竟给我们具体的编程工作带来了哪些考量呢?

首先,进程作为系统进行资源分配和调度的一个独立单位,这样的说法,细细品味,很有来头。巧妙就巧妙在,操作系统本身也是运行于底层硬件之上,而在一般计算机中,CPU又作为承接一切系统处理的核心,在这样的情况下,我们再看线程的介绍:“是CPU调度和分派的基本单位”,我们可以总结出如下事实:

  • 进程是操作系统概念,是操作系统调度的一个实体,操作系统的运行本身是抽象概念
  • 线程不但是操作系统范畴下的概念,也是给CPU具体调度的实体,CPU的运行本身是实体概念

操作系统本身的进程调度,也需要落实到线程给CPU执行,所以你品,你细品。由于实际的执行依托CPU,因而其实面向线程的相关操作,才是真正面向最终运行实体的操作。

看来,线程作为特殊的中间人,下启进程,上接协程。进程是给操作系统管理的,而协程是交由用户管理的。

其实,我们已经在原先进程、线程、协程的了解中,无意中引入了抽象链的概念。无论是进程,还是线程,抑或是协程,如果从实体的关系上来考虑,真正在CPU上化为电信号处理的,我们称之为线程,而另外两种概念都是针对操作系统和个人的基础之上抽象而来,这样的现实和我们接触的知识多多少少都感觉有所违背。但如果我们把一切都化为抽象,那么进程、线程、协程的关系却又清晰无比,层层嵌套,互不干扰。

进程提供完整资源,线程基于资源实际运作,协程依托线程运行,交由用户处理程序逻辑,自底向上,合理有序,正如下面的一条抽象链。

进程->线程->协程

止部抽象链

根据4.2节中的介绍,我们探讨并行这个概念的时候,研究的是事件的时间片重叠。想象如下的场景,如果两个进程,分别在两个不同的计算机上运行,他们之间没有任何交流,那么你需要考虑这两个进程之间通信、安全等问题吗?这和并发问题相关吗?

对这两个问题的回答显而易见都是否定的。两个进程完全隔离,并且在两台计算机上,彻底的、不受干扰的并行运算成为可能,而这也是大数据等技术带来的大规模并行计算的基础。冲突是什么时候引入的呢?

如果两个进程在同一个计算机上,如果我们只考虑这一台计算机,那么,这个和并发相关吗?联想起第一章和第二章提起的并发的基础哲学思想起源,我们有如下的愿景并发现如下的事实:

  • 我们希望两个进程更高效率的运行,最好是能完全并行。但是客观条件不允许,只有一台机器。

这就引入了矛盾,为了缓和这个矛盾,我们需要考虑并发问题了。好在,操作系统层面提供了关于进程的完整抽象,我们可以基于操作系统相关概念,对这个问题进行进一步的探讨。但不管怎么说,并发的问题开始被考虑了进来,而且似乎隐约能感觉到这并不是一个容易的问题。

由于进程是给操作系统提供的概念,为了解决实际运行的问题,我们需要进一步探讨线程相关的问题。到了线程这个层级,在4.1节中提及的那些问题,诸如竞争、死锁、活锁、饥饿锁等等,一一浮现。

而进一步的向下抽象到更加细化的调度粒度,是否还会引入更多的问题?答案又是否定的,结果你已经体会到了,借助Golang和其他支持协程的语言,通过引入协程的概念,使人们需要关心的问题越来越少了,编写正确的并发代码变得无比的简单。

通过某种逻辑构建的抽象理解链条,称之为抽象链。通过构建理解洼地,营造某些场景下的抽象链遍历局限,中止在途中某个位置,停止进一步对深层次问题的探讨,称之为止部抽象链。

不难发现,当我们开始向逐步细化资源分配的方向移动抽象层时,对事物的建模变得更加难以推理,抽象对我们来说变得越来越重要。 换句话说,获得并发权越困难,访问容易编写的并发原语就越重要。 不幸的是,大多数编程语言并发逻辑都是以系统线程作为最高抽象层次,恰好在问题最多的地方停住。

在Golang出现前,这是大多数流行编程语言的抽象链的最终解决方案。如果你想编写并发代码,你可以用线程来建模你的程序并同步它们之间的内存访问。如果你有很多事情需要同时建模,并且你的机器不能处理那么多的线程,你会创建一个线程池并将你的操作复用到线程池中。对于熟悉java的同学来说,这或许是轻车熟路,但对于大多数人来说,尤其是刚接触多线程编程的新手,我相信这犹如噩梦。

Golang的到来迎来了变革。通过止部抽象链在协程,编程人员对上层问题的解决停止在协程层面就是可以满足需求,Golang等语言底层的机制已经很好的为我们调度了相关工作。

并发并行分离

止部抽象链在协程,这一变革直接推动了并发并行分离思考问题的风潮。

协程使我们不必从并行的角度思考我们的问题,而是让我们对问题进行模拟,使其更接近自然。 虽然我们讨论了并发和并行之间的区别,但这种差异如何影响我们对解决方案的建模可能并不明确。 我们来看一个例子。

比方说,我们需要构建一个Web服务器,在客户端提交访问请求。暂时搁置具体使用框架,用一种只提供线程抽象的语言,我们可能需要考虑如下的问题:

  • 我的语言是否自然地支持线程,还是必须选择一个库?
  • 我的线程限制边界应该在哪里?
  • 此操作系统中的线程有多重?
  • 我的程序将如何在处理线程中运行不同的操作系统?
  • 我应该创建一个线程池来限制我创建的线程数量。 我如何找到对这个数量来说最佳的数字?

上面的这些问题,你会都感觉是言之凿凿的技术问题,这些问题也经常是面试过程中针对系统设计的老生常谈。许许多多的程序员以熟练掌握这些问题的最佳解决方案而引以为傲。确实,所有这些都是需要考虑的重要事情,但是——没有一个问题是直接关注你正在尝试解决的问题。

你被放到了如何解决并行性问题的技术上。

这灾难吗?这很灾难。程序是我们用来解决问题的工具和手段,绝对不是最终的目的。考虑一下我们的原始问题吧,简要来看,用户访问一个web服务器,涉及到的技术交互,用语言来描述应该如下:

  • 个人用户正在试图连接到我的服务器,开启一个HTTP会话。我负责的HTTP会话应该对他们的请求返回响应。

Golang直接就可以利用go关键字和相关的并发言语简洁干练的实现这个问题。我们用如下的伪代码来做一个案例吧:

func 网络请求到来(具体的网络请求 网络请求){
    go 这个请求对应的处理逻辑(具体的网络请求)
}

上面的例子,针对每一个网络请求,都开启一个对应的处理逻辑的协程。是不是很简单?是不是很贴近原始问题?

试看beego、gin等常见基于golang的服务器框架,哪一个像最初事无巨细的考虑线程抽象级别需要考虑的问题?答案是一个都没有!轻量级协程带来的百万,甚至千万级别调度能力,我们为每一个过来的请求都开一个协程就好了~

这是通过Golang对我们的承诺实现的:协程是轻量级的,我们通常不必担心创建一个导致耗费很多资源。有时候需要考虑系统中有多少个协程正在运行,正如在3.6可伸缩并发设计中我们提及的各种预测、监测,以及限制速率中所使用的令牌桶手段。但是这么做是一个过早的优化。我们在此处抛开先前提及的并发设计优化问题,仅仅将协程轻量化、抽象化这些特征与线程进行对比。

对问题的更直观自然的映射处理好处是巨大的,它也有一些有益的副作用。Golang的运行时自动将协程多路复用到OS线程,并为我们管理其调度。 这意味着可以在不需要改变我们如何模拟问题的情况下对运行时进行优化; 这是经典的关注点分离。 随着并行性的进步,Go的运行时在更新,程序的性能也在提高。你如果留意过Golang的发布说明,可能偶尔会看到类似的东西:

在Go XXXX 版本中,针对goroutines的调度顺序已经改变

或许,屏蔽线程级别的用户操作,让某些程序员感到不够心安。但当他们尝到具体为自己、为用户实现价值的甜头时,Who care?

我们总结一下。

前文阐述,从资源层面抽象,关于进程、线程、协程的层叠关系,也就是抽象链应该如下:

进程->线程->协程

而通过止部抽象链在协程,Golang为我们提供了更加简便的对现实生活问题解决流程的抽象,让我们把事情的所有处理逻辑落实到协程上:

处理逻辑->利用goroutine->落实到协程

而这样的分析过程,依托的是并发思想,开展的编程工作,统称并发编程。

Golang做了什么?运行时阶段,Golang编译后提供的能力,智能对任务序列进行调度:

协程->Golang屏蔽的调度逻辑->以较优效率调度到线程->CPU实际执行

如果全部时刻CPU上工作只能串行执行,那么论单台机器而言将不存在并行情况(少有的一点电信号切换过程,带来的价值可以忽略,近似认为没有并行)。这时候,并发编程带来的性能提升只能体现在带有其他系统交互的情况下,比如:

  • 需要人眼观测的系统(信号送出人体需要处理,带来人体与计算机之间的交互)
  • Web服务器(网络的传送和计算机分离了,传送过程和处理过程可以并行)

这也是为什么并发编程一般在web服务器优化空间很大的原因,因为即便只有CPU单核处理,得益于需要考虑计算机外部网络请求传送的相关数据,通过优化并发代码,一样可以获得较高的性能提升。

但如果机器本身CPU具有多核,也就是机器范畴内部存在CPU同时执行多个程序的可能,那么在单台机器上会产生如下的作用:

协程->Golang屏蔽的调度逻辑->以较优效率调度到各个线程->线程有可能由不同CPU核执行->实现客观并行

这样,在多核计算机下,即便是只考虑纯单机情况,逻辑并发代码也可以借助Golang的协程机制实现可观的性能提升。

全部评论

相关推荐

Twilight_m...:表格简历有点难绷。说说个人看法: 1.个人基本情况里好多无意义信息,什么婚姻状况、健康状况、兴趣爱好、户口所在地、身份证号码、邮政编码,不知道的以为你填什么申请表呢。 2.校内实践个人认为对找工作几乎没帮助,建议换成和测开有关的项目,实在没得写留着也行。 3.工作经历完全看不出来是干什么的,起码看着和计算机没啥关系,建议加强描述,写点你在工作期间的实际产出、解决了什么问题。 4.个人简述大而空,看着像AI生成,感觉问题最大。“Python,C,C++成为我打造高效稳定服务的得力工具”、“我渴望凭借自身技术知识与创新能力,推动人工智能技术的应用发展,助力社会实现智能化转型”有种小学作文的美感。而且你确定你个人简述里写的你都会嘛?你AI这块写的什么“深入研究”,发几篇顶会的硕博生都不一定敢这么写。而且你AI这块的能力和软测也完全无关啊。个人简述建议写你对哪些技术栈、哪些语言、哪些生产工具的掌握,写的有条理些,而且最好是和测开强相关的。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务