翻译原文链接 转帖/转载请注明出处
英文原文链接 发表于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. mp4格式的视频,编码方式mpeg4,转化为h264

    知识点:在使用vcastr3.swf播放器播放flv视频,(同时在html5页面,使用<video>标签时),发现某些MP4格式的代码不能播放 原因:vcastr3.swf和video,不 ...

  2. 在Linux Centos 7.2 上安装指定版本Docker。

    相关资料链接: https://docs.docker.com/install/linux/docker-ce/centos/#install-docker-ce 先清空下“历史” yum remov ...

  3. python一个元素全为数的列表做差分

    woc = [7, 5, 7, 3, 5, 1, 2] diff = [ wo[i]-wo[i+1] for i in range(len(wo)-1) ]

  4. Apache的MaxClients设置

    本文将介绍Apache的MaxClients参数的重要性以及在GC发生时对系统整体性能的显著影响.通过几个例子,你将会更清晰的理解MaxClients值所引发的问题.最后会介绍如何依据系统的可用内存来 ...

  5. Java程序员怎么迈向架构师

    怎样学习才能从一名Java初级程序员成长为一名合格的架构师,或者说一名合格的架构师应该有怎样的技术知识体系,这是不仅一个刚刚踏入职场的初级程序员也是工作三五年之后开始迷茫的老程序员经常会问到的问题. ...

  6. 正确使用iOS常量(const)、enum以及宏(#define)

    前言:本文主要梳理iOS中如何使用常量.enum.宏,以及各自的使用场景. 重要的事情首先说:在iOS开发中请尽量多使用const.enum来代替宏定义(#define):随着项目工程的逐渐增大,过多 ...

  7. 在Web API 2 中实现带JSON的Patch请求

    译文:http://www.cnblogs.com/kexxxfeng/p/the-patch-verb-in-web-api-2-with-json.html 原文:https://carly.io ...

  8. Linux虚拟机忘记root密码的拯救办法

    于是百度之,还是有很多解决方案的.其原理就是,进入到single单用户模式下,去修改root密码: 1.重启系统,选择Linux引导,然后按住e,出现如下界面: 按下e 出现如下页面 选择kernel ...

  9. java:历史回顾

    1.String和StringBuffer区别 2.Runtime和System类,包括对象垃圾收集 Rumtime.gc() System.gc() 调用的其实就是Runtime的gc回收 3.da ...

  10. 【error】: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

    错误描述如下: error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) ...