你等或者不等,协程拥有自己的寄存器上下文和

作者: 编程  发布:2019-09-30

前言

随着容器技术的兴起,越来越多不同类型的应用开始使用容器的方式进行交付。Golang作为服务器端非常热门的一门语言同时也是容器技术的主要编写语言备受关注。那么将一个Golang应用进行容器化的时候,需要注意哪些事情,在出现问题时该如何进行调优和诊断呢?

导读

要理解进程与线程,首先得了解并发与并行。

翻译原文链接   转帖/转载请注明出处

  现在多进程多线程已经是老生常谈了,协程也在最近几年流行起来。python中有协程库gevent,py web框架tornado中也用了gevent封装好的协程。本文主要介绍进程、线程和协程三者之间的区别。

先谈谈Golang本身的设计

Golang是谷歌发布的第二款开源编程语言。Golang专门针对多处理器系统应用程序的编程进行了优化,使用Golang编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。Golang在容器相关的场景和领域以及高并发的服务器程序场景下扮演着非常重要的角色。

Golang具有如下三个特点:

  • 简洁 快速 安全
  • 并行 有趣 开源
  • 内存管理 数组安全 编译迅速

在学习一门语言前,通常我会主要关注如下三个方面:第一这门语言的特性是什么;第二这门语言解决的场景和问题是什么;第三这门语言的内部设计是否有需要注意的地方。上面的介绍已经为我们解答了第一个和第二个问题,那么接下来我们主要来讨论第三个问题。那么Golang的这些优秀的特性内部的设计方式是什么样子的,使用起来是否有什么需要特别注意的呢?为了详细解答这个问题,我们将问题拆分成了二个部分分别为大家解答。

  • Golang是如何实现并行的

高并发是Golang被大家接纳和认可的最重要一环。对于大型的互联网项目而言,高并发可以说是应用性能的立足之本,再棒的功能与特性也不如稳定运行来得让人安心。从前大家在关注C10K问题,而现在越来越多的人开始思考如何解决C10M问题。从C10K问题到C10M问题,解决问题的方式已经不是简简单单的调整内核参数那么简单的。更多的是要从架构甚至应用自身的角度来解决,一个高效的并发模型,可以从应用程序的交付压榨系统的性能。目前比较成熟的并发模型,主要是通过进程、线程与协程三种不同方式来进行实现的。

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

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

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

Golang的并发模型是基于协程的,而协程在Linux底层的调度是依赖进程的调度的,而这之间的转换都通过Golang自身的调度器进行了管理,无需开发者关心。但是这个时候有经验的开发者就会提出问题了,golang本身是编译型的语言没有类似JVM一样的虚拟机可以在运行时指定参数,那么Goroutine这种方式是否有参数需要设置来保证性能。

此处给大家讲述一个关于Goroutine栈扩容的问题,我们内部有一个全双工的高并发写离线数据的服务,在底层数据出现消费慢的时候快速出现OOM,问题产生的原因就是由于Goroutine栈扩容,最后可以通过通过拆分Goroutine的逻辑到上半段和work group的方式实现,由于篇幅的原因不过多的赘述,可以参考如下这篇博客更深入了解栈扩容的问题。

  • Golang的内存是如何管理的

内存管理对于C++与Java的开发者而言是最熟悉不过的了。C++的开发者必须通过代码手动的申请与释放内存,因此必须熟悉内存布局和使用;Java的开发者虽然有JVM帮助进行内存的管理与回收,但JVM不同的内存参数配置会导致程序因为回收内存带来不同的性能表现。而Golang作为一门高级编程语言,同样无需开发者直接操作内存,但是Golang中的GC设计是存在一些缺欠的。主要的问题在GC时的卡顿上,具体的问题可以参考如下文章,不过这点也无需大家特别关心,建议直接使用Golang1.9以后的版本进行编译即可。深入了解Golang的GC可以参考如下文章

并发与并行

  • 并发
    单核CPU时间分片,多个程序切换执行,这就是并发。并发是共享内存的,所以需要加锁。
    因为并发共享内存,并且会几个程序互相切换执行,所以在一个CPU执行的并发必须处理上下文切换的问题。 进程就是在这种背景下,被提出的。进程就是一些相关线程的统称,是一些相关线程的集合。进程搭配虚拟内存、进行表等,可以管理独立程序的运行、切换。
    并发也就是多线程。

由上面这段话可见,程序在运行过程中,对计算机资源的分配是个很重大的问题,于是出现了操作系统专门干这个活。操作系统核心的操作是陷入内核(Kernel),切换到操作系统,让内核来做。

  • 并行
    多核CPU几个程序同时执行,这就是并行。并行也就是我们所说的多进程。

英文原文链接 发表于2014/02/24

一、概念

Golang容器化建议

  • 常规容器化建议

首先需要进行的是常规的容器化优化,具体的内容可以参考如下文章进行体积的精简和优化。

  • Golang 中DNS的问题

不同语言对于DNS的Lookup处理会有所不同,在Java或者Node.JS等常见的语言和框架中对DNS Lookup都提供语言级别的内置的Cache,而在Golang中却不存在类似的能力,这会导致对于高并发的场景中,Golang程序有可能会出现大量的DNS查询,而在kubernetes中,DNS是通过内部的coredns或者kube-dns的方式提供的,因此有可能会因为大流量的Golang DNS导致集群异常,为了解决这个问题,建议开发者在Golang的Dockerfile中集成nscd进行DNS的Cache,具体的操作步骤可以参考如下文档

  • Golang 中GC的问题

在本文的上面的部分,为大家讲解了Golang GC的一些缺欠以及如何避免GC问题的方式,在容器化的时候是否还需要做其他的优化呢?面对内存的异常,我们要如何定位是一个GC的问题呢?这里要给大家介绍的是Golang自来的pprof,pprof是Golang语言中内置的性能调优工具,可以协同Flame-Graph,排查CPU性能、内存性能、GC回收等问题,建议在容器的场景中,在代码中集成pprof,并通过环境变量的方式进行开关设置,容器的Dockerfile中保留端口的保留,当出现问题的时候可以设置环境变量的方式进行开启,快速进行线上问题的诊断。pprof的使用,可以参考如下文章

  • Golang 中CGO的问题我们知道Golang作为一门编译型的语言,可以通过开启CGO的标签,直接使用C的代码并编译为二级制文件直接使用。但是这种方式非常不建议在容器中开启,特别是使用类似Alpine这种最小镜像的场景下。因为开启CGO的场景下,会动态链接系统的C库,而在Alpine上,很多的目录布局是有所差异的,另外有些最简化的版本glibc的支持并不完善,因此非常不建议使用CGO的方式编译Golang在容器中使用。
  • Golang 中监控的建议容器中很多监控的方式都无法很好的直接复用,建议大家使用更Docker的方式来解决,例如使用Prometheus的方式暴露Golang应用内部的指标进行监控,这也是目前非常多Golang开源项目的标配了。使用方式参考下文,与容器服务结合可以参考如下文章
  • Golang 中性能的问题Golang与容器的结合通常是为了高性能的场景,那么通常需要对内核参数进行部分的调整,具体的调整方式可以参考如下文章

进程和线程

  • 进程
    进程是一个容器,也就是一个程序。
    进程的切换:
  1. 切换页全局目录(Page Global Directory)来加载新的地址空间,实际上会加载新进程的cr3寄存器值。
  2. 切换内核堆栈和硬件上下文,这些包含了内核执行一个新进程的所有信息,包含了CPU寄存器。
  • 线程
    线程是容器里的工作单元。上面说并发的时候讲到,并发是共享内存的,也就说这些并发的线程会共享一个地址空间。线程的切换,不需要重新加载地址空间,页面缓冲区,需要切换寄存器上下文和栈。开销相对较小。

    线程在切换任务的时候,切换寄存器上下文和栈是抢占式的(Preeemptive multitasking),谁抢了是谁的,这就导致了线程之间的执行顺序是无法保证的,所以使用线程时需要小心操作同步问题。

    进程和线程的相同点

    线程和进程的切换,都需要陷入系统调用,即CPU先跑操作系统的调度程序,然后再由调度程序决定该炮哪一个进程(线程)

Go语言

如果你刚刚接触Go语言,或者说你并不理解“并发不等于并行”这句话的含义,那么Rob Pike的讲座值得一看(在youtube上)。这个视频有30分钟长,我保证花30分钟看这段视频是非常值得的。

这里摘录一段他提到的并发和并行之间的区别:“当大家听到并发这个词的时候,他们往往想到的是并行。并行是一个相关,但却完全不同的概念。当我们编程的时候,并发指的是多个独立运行的进程,而并行是指同时运行的多个计算。并发是为了一下子处理很多东西。并行是为了同时做很多事情。” [1] (注:这里的概念有点绕。其实本质的区别在“同时”这个词上。并行强调的时候几个进程同时进行。而并发指的是运行多个进程,但这些进程并不需要同时被执行。它们可以是被调度在同一个CPU分时运行的。)

Go为我们写并发程序提供了便利。它提供了goroutine以及它们之间通信的功能。在这里我们主要讨论goroutine。

  1、进程

最后

Golang相对而言算是非常”省心“的一门语言了,在老版本的Golang中还需要通过runtime设置GOMAXPROCS,但是在最新版本的Golang中已经基本无需关心runtime的任何参数设置了,这些参数就像nginx的auto一样,会随着探测的配置自动变化,而在容器中,我们依然需要GOMAXPROCS,因为GOMAXPROCS的识别方式是通过获取系统资源的方式确定的,而在容器中是通过只读挂载宿主机的文件实现的,因此获取的资源还是宿主机的数值。因此,Golang的应用容器化,更多的还是要做好标准镜像优化的步骤,以及在代码级别做好避免触发GC和Goroutine的问题。

云服务器99元拼团购!拉新还可赢现金红包!300万等你瓜分!马上一键开团赢红包:

本文作者:莫源

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

同步和异步

首先要明确一点,同步/异步这个讨论对于IO密集型才有意义。
因为对于计算密集型程序来说,
你等或者不等,
计算任务都在那里,
不多不少。
对于IO密集型程序来说,
你等就是占着茅坑不拉屎。你的肚子还没有应答,你蹲在那有什么用?反而不如腾出地方来,让程序别的地方先用着。等你确实要拉了,再来。这个坑位,就是CPU啊!

Goroutine和线程的区别

Go语言使用的是goroutine,而像Java这样的语言大多使用线程。它们之间的区别是什么呢?让我们从三个方面来看看它们的区别:内存占用,创建和销毁,以及切换开销。

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

协程

协程,coroutine,也叫纤程(Fiber),或绿色线程。
协程是用户态的轻量级线程。这句话是什么鬼?

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,切回来的时候,挥发原来保存的寄存器上下文和栈。

由此可见,协程能保留上一次调用时的状态。协程的任务切换是由用户自己控制的,不存在抢占,所以这种叫做协作式多任务

协程也是单线程的,本质也是异步+回调,但是它是经过包装的,写出的代码看着是同步的代码。写过Node的应该深有体会。

Go研究出来了一套协程的调度算法,所以Go的协程叫做Goroutine。
Kotlin也有,C#也有,不是什么新鲜玩意。但是在语言的层面上的支持,还是第一个。

内存占用

创建一个goroutine不需要太多的内存 - 大概2KB左右的栈空间。如果需要更多的栈空间,就从堆里分配额外的空间来使用。[2][3] 新创建的线程会占用1MB的内存空间(这大约是goroutine的500倍)。这还不包括守护页(guard page)的空间。守护页是用来保护线程之间的内存空间不会被相互窜改。[7]

因此一个处理很多请求的服务可以为每个请求创建一个goroutine。但是如果为每个请求去创建一个线程,那么它很快就会碰到OutOfMemoryError。这不是Java独有的问题,任何使用操作系统线程作为主要并发手段的编程语言都会碰到这个问题。

  2、线程

事件驱动

事件驱动是事先编写一个事件循环,这个事件循环程序不断地检查目前要处理的信息,多用在GUI框架、页面上的JS事件。如果这个事件收到了要处理的事件的信号,就异步去执行。

然后这个信号是推拉结合的。

基于事件驱动的编程是单线程思维,特点是异步+回调。

创建和销毁的开销

线程需要从操作系统里请求资源并在用完之后释放回去,因此创建和销毁线程的开销非常大。为了避免这些开销,我们通常的做法是维护一个线程池。Goroutine的创建和销毁是由运行环境(runtime)完成的。这些操作的开销就比较小。Go语言不支持手工管理goroutine。

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

协程的优劣

切换开销

当一个线程阻塞的时候,另外一个线程需要被调度到当前处理器上运行。线程的调度是抢占式的(preemptively)。当切换一个线程的时候,调度器需要保存/恢复所有的寄存器。这包括16个通用寄存器,程序指针(program counter),栈指针(stack pointer),段寄存器(segment registers)和16个XMM寄存器,浮点协处理器状态,16个AVX寄存器,所有的特殊模块寄存器(MSR)等。当在线程间快速切换的时候这些开销就变得非常大了。

Goroutine的调度是协同合作式的(cooperatively)。当切换goroutine的时候,调度器只需要保存和恢复三个寄存器

  • 程序指针,栈指针和DX。切换的开销就小多了。

前面已经谈到了,goroutine的数目会比线程多很多,但这并不影响切换的时间。有两个原因:第一,只有可以运行的goroutine才会被考虑,正在阻塞的goroutine会被忽略。第二,现代的调度器的复杂度都是O(1)的。这意味着选择的数目(线程或者是goroutine)不会影响切换的时间。[5]

  3、协程

协程的优点

  • 跨平台/跨体系架构
  • 与进程/线程相比,上下文切换的开销
  • 与线程相比,不需要原子操作锁定及同步
  • 与事件驱动相比,方便切换控制流,编程模型简单
  • 高并发,一个CPU支持上万的协程

Goroutine的运行

前面谈到,运行环境负责goroutine的创建,调度和销毁。运行环境被会分配一些线程,用来运行所有的goroutine。在任何一个时间点,每个线程只会运行一个goroutine。如果一个goroutine被阻塞,另外一个goroutine会来替换它在对应的线程上运行。[6]

因为goroutine的调度是协同合作式的,如果一个goroutine不停的循环,其它的goroutine就没有机会被调度运行了。在Go 1.2里,这个问题的解决办法是在调用一个函数的时候去偶尔触发Go的调度器。这样一个循环里如果调用了没有被内联的函数,它就可以被抢占了。

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

协程的缺点

  • 无法利用多核资源,事件驱动,本质是个单线程的循环而已。需要配合进程来执行。

9159.com,Goroutine的阻塞

Goroutine是廉价的,在下面这些阻塞情况下它们也不会造成运行的线程被阻塞:

- 网络收发

- 睡眠

- channel操作

- sync包里的一些会阻塞的基本操作

即使创建了成千上万的goroutine并且大多数被阻塞了,也不会造成太多的系统资源浪费。因为运行环境会调度另外的goroutine来运行。

简而言之,goroutine是对线程的轻量化抽象。Go语言的程序员不需要直接操作线程。与此同时操作系统也不知道goroutine的存在。从操作系统的角度来看,一个Go程序有点像一个事件驱动的C程序。[5]

 

演进轨迹

IO密集型程序:多进程 -> 多线程 -> 事件驱动 -> 协程
CPU密集型程序:多进程 -> 多线程

  1. 为什么从多进程到多线程是一种进步?
    因为线程比进行性能开销小,而且多线程之间数据通信与同步更加方便。想同步的时候就访问,修改的时候加把锁就可以了。

    这个也是符合我们直觉的。比如要需要一个程序某个部分的几个不同运算值,是在后台多起几个进程分别跑呢?还是写成多线程处理呢?实践中,我们很少见到开多进程的吧?

  2. 那为什么CPU密集型任务没有发展到事件驱动、协程呢?
    因为CPU密集型的任务,一般都是需要计算的,计算是需要CPU的。CPU的多少是和系统的硬件相关,异步是改变不了这一点的,等待的时候不能计算,等待完了该算的还得接着算,所以说没什么卵用。

线程和处理器

虽然我们不能直接控制运行环境创建多少线程,我们可以设置程序使用的处理器核数。这是通过调用runtime.GOMAXPROCS(n)函数设置GOMAXPROCS变量来实现的。(注:也可以通过直接设置环境变量来控制)。增加处理器核数并不意味着程序性能的提高。这取决于程序本身的设计。你的程序需要用到多少个内核数可以用剖析(profiling)工具来找到答案。

二、区别:

多进程(并行)/多线程(并发)/协程各自的适用场景

结束语

和其它语言类似,避免多个goroutine同时访问一个共享资源是非常重要的。goroutine之间,最好是用channel来传输数据。有兴趣的可以读一读“do not communicate by sharing memory; instead, share memory by communicating”。

最后,我强烈推荐读一下C. A. R. Hoare写的“Communicating Sequential Processes”。他是个天才。在这篇论文(1978年发表的)里,他预测了单核处理器性能最终会遇到瓶颈,然后芯片制造商们会增加处理器的内核数。他的思想对Go语言的设计影响深远。

  1、进程多与线程比较

为什么程序多了电脑卡

进程多了,操作系统会频繁切换进程。从上面可知,进程一切换,页全局目录、内核堆栈、硬件上下文都会变。一旦进程变多之后,频繁切换会挤占大部分资源。

参考文献

  1. Concurrency is not parallelism by Rob Pike

  2. Effective Go: Goroutines

  3. Goroutine stack size was decreased from 8kB to 2kB in Go 1.4

  4. Goroutine stacks became contiguous in Go 1.3

  5. Scheduling of goroutines by Dmitry Vyukov

  6. Analysis of the Go runtime scheduler

  7. 5 things that make Go fast by Dave Cheney

线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别:
1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
3) 线程是处理器调度的基本单位,但进程不是
4) 二者均可并发执行

相关知识补充

9159.com 1

image.png

啥是“陷入内核”?看到这个词的第一反应是写错了。其实不然。
要说请首先需明确,特权级的概念。就行linux系统里面,喜欢划分管理员和用户等不同权限角色一样。在系统中,为了让资源调配集中管理,便有了特权级这个概念。特权级一共有0-3这四个级别,0最高,可以管理CPU。
linux系统里面只有两个级别,0级和3级。操作系统的内核当然是最高级0级,应用程序是3级。当应用程序需要访问系统资源时,CPU就进入了内核,从图上看好像陷进入一样。

所谓的上下文,是指程序在运行过程中的一些中间状态,比如会运算出来一些值,这些值在后面的计算中也会用到。当然这些值必须保存在内存中。

5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

  2、协程多与线程进行比较

1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。

2) 线程进程都是同步机制,而协程则是异步

3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

 

 三、进程和线程、协程在python中的使用

  1、多进程一般使用multiprocessing库,来利用多核CPU,主要是用在CPU密集型的程序上,当然生产者消费者这种也可以使用。多进程的优势就是一个子进程崩溃并不会影响其他子进程和主进程的运行,但缺点就是不能一次性启动太多进程,会严重影响系统的资源调度,特别是CPU使用率和负载。使用多进程可以查看文章《python 多进程使用总结》。注:python2的进程池在类中的使用会有问题,需要把类函数定义成全局函数。具体可参考

  2、多线程一般是使用threading库,完成一些IO密集型并发操作。多线程的优势是切换快,资源消耗低,但一个线程挂掉则会影响到所有线程,所以不够稳定。现实中使用线程池的场景会比较多,具体可参考《python线程池实现》。

  3、协程一般是使用gevent库,当然这个库用起来比较麻烦,所以使用的并不是很多。相反,协程在tornado的运用就多得多了,使用协程让tornado做到单线程异步,据说还能解决C10K的问题。所以协程使用的地方最多的是在web应用上。

总结一下就是IO密集型一般使用多线程或者多进程,CPU密集型一般使用多进程,强调非阻塞异步并发的一般都是使用协程,当然有时候也是需要多进程线程池结合的,或者是其他组合方式。

 

本文由9159.com发布于编程,转载请注明出处:你等或者不等,协程拥有自己的寄存器上下文和

关键词:

上一篇:没有了
下一篇:没有了