概述
Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与进程、线程、协程等不同。它是一个goroutine。 ——Rob Pike
并发(concurrency)与并行(parallellism)
"Concurrency is not Parallelism", 并发不是并行。
Erlang 之父 Joe Armstrong的解释:
- 并发是两个队列交替使用一台咖啡机
- 并行是两个队列同时使用两台咖啡机
一种更加严谨的理解来自于UNIX之父也是Golang主要作者之一的Rob Pike:
- 并发(Concurrency)是同时处理很多事情(dealing with lots of things at once)
- 并行(Parallelism)是同时执行很多事情(doing lots of things at once)
二者有相关度,但并非同一个概念:并发可认为是一种逻辑结构的设计模式。你可以用并发的设计方式去设计模型,然后运行在一个单核系统上,通过系统动态地逻辑切换制造出并行的假象。此时,你的程序不是并行,但是是并发的。你可以将这种模型不加修改地运行在多核系统上,此时你的程序可以认为是并行。此处,并行更关注的是程序的执行(execution)。
来自神书CSAPP的用数学语言描述的回答:
- 并发(Concurrency)是说进程B的开始时间是在进程A的开始时间与结束时间之间,我们就说A和B是并发的。
- 并行(Parallel Execution)是并发的真子集,指同一时间两个进程运行在不同的机器上或者同一个机器不同的核心上。
无论是并发还是并行,都是针对多任务场景的。
- 并发是多任务在逻辑上同时运行(借助调度器可以在单核心模拟所谓同时运行)
- 并行是多任务真的在同时运行。必要条件:可调度核心数大于一
总核心数 = CPU数 * CPU核心数
进程(process)与线程(thread)
进程是资源分配的基本单位。用来管理资源(例如:内存,文件,网络等资源) 倘若一台机器有多个CPU,进程可以用来压榨它们(以CPU为基本治理单位) 单CPU中进程只能是并发,多CPU计算机中进程可以并行。
线程是独立运行和独立调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。倘若有多个CPU或是一个CPU有多个核心,线程可以用来压榨它们(以core为基本治理单位)单CPU单核中线程只能并发,单CPU多核中线程可以并行。
线程依赖于进程而存在,一个进程至少有一个线程。
进程与线程的区别(经典操作系统面试题):
1、地址空间与系统资源
进程是资源分配的基本单位。进程有自己的独立地址空间,线程共享所属进程的地址空间;进程是拥有系统资源的一个独立单位,而线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),和其他线程共享本进程的相关资源如内存、I/O、cpu等。
2、调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
3、系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
4、通信
进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信
5、鲁棒性
一进程多线程程序一个线程炸全都炸,但多进程程序中一个进程崩溃并不会对其它进程造成影响,因为进程有自己的独立地址空间。因此,多进程更加健壮。
协程(coroutine)与goroutine
协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理(进程和线程的调度由操作系统进行),而完全是由程序所控制(也就是在用户态执行)。
协程最重要的特点是单线程执行。简单来说,协程是并发不并行的。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
1、极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
2、不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
goroutine到底是不是协程?
严格来讲,goroutine不是协程。
《The Go Programming language》中对以goroutine的描述是这样的:
在Go语言中,每一个并发的执行单元叫做一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。
goroutine不是协程的最主要的理由:
在多核心的环境下(GOMAXPROCS != 1),goroutine是可以并行的(也就是可以在多个线程上运行)。而协程是单线程的,不能并行。
那goroutine到底是用来干嘛的呢?我的回答是:替代线程。
goroutine和线程又有着怎样的区别呢?为什么golang之父们要重新造轮子呢?
主要原因有四点:
1、动态栈:
每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费,但对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率,允许创建更多的线程,或者可以允许更深的递归调用,不过这两者是没法兼备的。
相反,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量,但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。
2、Goroutine调度
OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度,所以从一个线程向另一个“移动”需要完整的上下文切换,也就是说,保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。
和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多(系统开销小)。
3、GOMAXPROCS
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的(这也就是为什么我们认为goroutine是与系统线程平级的)。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计算在内。
4、goroutine无ID号(简而言之,严防奇技淫巧与coder炫技)
在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被很容易地获取到,典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。
goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。比如说,一个web server是用一种支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP请求的信息,这代表它们就是去其存储层(这个存储层有可能是tls)查找的。这就像是那些过分依赖全局变量的程序一样,会导致一种非健康的“距离外行为”,在这种行为下,一个函数的行为可能并不仅由自己的参数所决定,而是由其所运行在的线程所决定。因此,如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。
Go鼓励更为简单的模式,这种模式下参数对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。
我们可以直接通过go语言本身提供的机制来为goroutine分配CPU核心。也就是说goroutine和线程是平级的!
goroutine基于协程也就是coroutine的思想,但远比协程甚至其它语言中的线程更加强大。可以将goroutine理解为封装好的自动化协程,而且能够参与CPU的core资源分配。
相应的,在golang开发中,我们一般采用多进程+多goroutine的方案,而非Java生态中多进程+多线程的方案
练习
使⽤两个 goroutine 交替打印序列,⼀个 goroutine 打印数字, 另外⼀ 个 goroutine 打印字⺟, 最终效果如下: 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122...
More
十分钟搞定goroutine mp.weixin.qq.com/s/IBjkv7Bw_…