翻译原文链接 转帖/转载请注明出处
英文原文链接 发表于2014/02/24

Go语言

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

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

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

Goroutine和线程的区别

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

内存占用

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

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

创建和销毁的开销

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

切换开销

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

Goroutine的调度是协同合作式的(cooperatively)。当切换goroutine的时候,调度器只需要保存和恢复三个寄存器 - 程序指针,栈指针和DX。切换的开销就小多了。

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

Goroutine的运行

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

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

Goroutine的阻塞

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

  • 网络收发

  • 睡眠

  • channel操作

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

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

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

线程和处理器

虽然我们不能直接控制运行环境创建多少线程,我们可以设置程序使用的处理器核数。这是通过调用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. 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

Goroutine是如何工作的?的更多相关文章

  1. [Go] golang缓冲通道实现管理一组goroutine工作

    通道1.当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道2.无缓冲通道和有缓冲通道,make的第二个参数就是缓冲区大小3.无缓冲通道需要发送和接收都准备好,否则 ...

  2. 第三章 Goroutine调度策略(16)

    本文是<Go语言调度器源代码情景分析>系列的第16篇,也是第三章<Goroutine调度策略>的第1小节. 在调度器概述一节我们提到过,所谓的goroutine调度,是指程序代 ...

  3. Go part 8 并发编程,goroutine, channel

    并发 并发是指的多任务,并发编程含义比较广泛,包含多线程.多进程及分布式程序,这里记录的并发是属于多线程编程 Go 从语言层面上支持了并发的特性,通过 goroutine 来完成,goroutine ...

  4. goroutine 调度算法

    自从开始使用 Go 语言,到现在也有一年多了,虽不算精通,但也算小有理解.在这里简单记录一下我的心得(其实是学习别人的心得) goroutine,Go 语言中 cpu 运行的最小单元,与 lua 携程 ...

  5. goroutine 分析 协程的调度和执行顺序 并发写

    package main import ( "fmt" "runtime" "sync" ) const N = 26 func main( ...

  6. goroutine 分析 协程的调度和执行顺序 并发写 run in the same address space 内存地址 闭包 存在两种并发 确定性 非确定性的 Go 的协程和通道理所当然的支持确定性的并发方式(

    package main import ( "fmt" "runtime" "sync" ) const N = 26 func main( ...

  7. go语言程序设计学习笔记-1

    https://www.jb51.net/article/126998.htm go标准库文档https://studygolang.com/pkgdoc 1. 如果想要再本地直接查看go官方文档,可 ...

  8. golang 面向对象编程

    概述 Golang语言的面向对象与c++,py等语言有所不同,是由于Golang不支持继承:与上述支持聚合和继承的面向对象的语言不同,Golang只支持聚合(也叫做组合)和嵌入.聚合和嵌入的区别: t ...

  9. Golang 并发Groutine实例解读(二)

    go提供了sync包和channel机制来解决协程间的同步与通信. 一.sync.WaitGroup sync包中的WaitGroup实现了一个类似任务队列的结构,你可以向队列中加入任务,任务完成后就 ...

随机推荐

  1. 分分钟解决 MySQL 查询速度慢与性能差

    一.什么影响了数据库查询速度 1.1 影响数据库查询速度的四个因素 1.2 风险分析 QPS: QueriesPerSecond意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的 ...

  2. Lucene TF-IDF 相关性算分公式

    转自: http://lutaf.com/210.htm Lucene在进行关键词查询的时候,默认用TF-IDF算法来计算关键词和文档的相关性,用这个数据排序 TF:词频,IDF:逆向文档频率,TF- ...

  3. 51 Nod 1091 线段的重叠

    2017-09-24 19:51:41 writer:pprp 上一个题目就是关于线段重叠最大值,这个是找区间最长重合? 给你n个线段,然后让你在其中选择两条,使两条尽可能重合多一点 解决方法; 1. ...

  4. Ubuntu 安装 networkx

    参考:ubuntu 下NetworkX的安装和使用 Dependences pip setuptools Commands 1.install networkx sudo pip install ne ...

  5. 使用阿里云ECS安装HDFS的小问题

    毕设涉及HDFS,理论看的感觉差不多了,想搭起来测试一下性能来验证以便进行开题报告,万万没想到装HDFS花费了许多天,踩了许多坑,记录一下. 背景:使用两台阿里云学生机ECS,分处不同账号不同区域,一 ...

  6. Excel转化成DataTable实现:NPOI和OLEDb

    使用两种方式实现的excel数据转化成DataSet,再结合前一篇的DataTable转化为实体,就可以解决excel到实体之间的转化. 代码如下: 首先定义一个接口: public interfac ...

  7. pathway一些网站

    1.BioCarta_Pathways https://cgap.nci.nih.gov/Pathways/BioCarta_Pathways

  8. .Net遍历窗体上控件

    实现遍历窗体上的控件以及找出TextBox控件,代码如下: foreach( Control control in this.Controls ) { if( control is TextBox ) ...

  9. 利用javascript实现页面截图

    html2canvas可以通过纯JS对浏览器端经行截屏,但截图的精确度还有待提高,部分css不可识别,所以在canvas中不能完美呈现原画面样式 兼容性: Firefox 3.5+ Google Ch ...

  10. 网页上播放音频、视频Mp3,Mp4

    昨天在处理网页上播放音频mp3,视频mp4上用了一天的时间来比较各种方案,最终还是选择了HTML5的 标签,谷歌浏览器.IE浏览器对标签的支持都很好,火狐上需要安装quicktime插件,效果比较差. ...