如果要我从 Go 的特性中选一个最棒的,那么必定是它内置的并发模型。Go 不仅支持并发,而且做的相当好。Go 语言并发模型和并发性的关系就好像是 Docker 和虚拟化一样。
## 什么是并发?
在计算机编程中,**并发性是计算机同时处理多个任务的能力**。例如,如果您在浏览器中上网,同一时刻可能会发生很多事情。在特定情况下,你可能会在你**当前浏览的网页上**听歌的同时下载一些文件。因此浏览器需要能够同时处理很多事情。如果浏览器无法立即处理它们,您需要等到所有下载完成后再开始浏览互联网。 那会令人沮丧。
通用 PC 可能只有一个 CPU 核心,却可以完成所有处理和计算。CPU 核心一次可以处理一件事。当我们谈论并发时,其实我们一次只做一件事,但我们将 CPU 时间片划分给需要处理的任务。因此,在现实中,我们可能同时感受到多件事情的发生,其实一次只做一件事。
让我们看一下 CPU 管理 Web 浏览器时是如何管理我们所讨论的示例中的内容。
![](https://raw.githubusercontent.com/studygolang/gctt-images/master/acheiving-concurrency-in-go/1.jpg)
因此,从上图中,您可以看到单个核心处理器几乎根据每个任务的优先级划分工作量,例如,页面滚动,听音乐可能具有低优先级,因此有时您的音乐可能会因为低网速而停止,但您仍然可以滚动页面。
## 什么是并行性?
但问题出现了,如果我的 CPU 有多个内核怎么样?如果一个处理器有多个内核,那么它称为多核处理器。您可能在购买笔记本电脑,个人电脑或智能手机时听说过这个术语。多核处理器能够同时处理多个任务。
在之前的 Web 浏览示例中,我们的单核处理器必须在不同的任务之间划分 CPU 时间片。使用多核处理器,我们可以在不同的核心中同时运行单独的任务。让我们用下图来解释它。
![](https://raw.githubusercontent.com/studygolang/gctt-images/master/acheiving-concurrency-in-go/2.jpg)
并行运行不同的东西的概念称为 ` 并行性 `
当我们的 CPU 有多个内核时,我们可以使用不同的 CPU 内核同时执行多个操作。因此,我们可以说我们可以很快完成一项工作(包括许多事情),但事实并非如此。等会我会回到这一点。
## 并发与并行
Go 建议仅在一个核心上使用 `goroutines`,但我们可以修改 Go 程序以在不同的处理器核心上运行 `goroutine`。现在,将 `goroutines` 视为 Go 的功能,因为它们就是,但还有其他更多功能。
并发和并行之间存在一些差异。**虽然并发性可以同时处理多个任务,但并行性却可以同时做多个任务**。并行性并不总是有利于并发,我们会在即将到来的课程中讲解这一点。
在这一点上,可能有很多问题在脑海中浮现,你可能已经有了并发的想法,但你可能想知道如何实现它以及如何使用它。要了解 Go 的并发体系结构以及如何在代码中使用它,以及何时在应用程序体系结构中使用它,我们就需要了解计算机进程是什么。
## 什么是计算机进程?
当您使用 C,java 或 Go 等语言编写计算机程序时,它只是一个文本文件。但是,由于您的计算机只能理解由 0 和 1 组成的二进制指令,因此您需要将该代码编译为机器语言。这就是编译器的用武之地。在 python 和 JavaScript 等脚本语言中,解释器也会做同样的事情。
当一个编译后的程序被发送到操作系统去处理时,操作系统会分配不同的东西,如内存地址空间(*进程的堆和堆栈的位置*),程序计数器,PID(进程 ID)和其他非常关键的东西。一个进程至少有一个线程称为主线程,而主线程可以创建多个其他线程。当主线程执行完毕后,进程退出。
所以我们理解该进程是一个容器,它具有已编译的代码,内存,不同的操作系统资源以及其他可以提供给线程的东西。简而言之,**进程就是内存中的一个程序**。但是什么是线程,他们的工作是什么?
## 什么是线程?
线程是进程内的轻量级进程。线程是一段代码的实际执行者。线程可以访问进程提供的内存,OS 资源句柄和其他内容。
在执行代码时,线程将变量(数据)存储在称为 stack 的内存区域中,这个区域会划分变量持有临时空间的地址。 堆栈是在编译时创建的,通常是固定大小,最好是 1-2 MB。而线程的堆栈只能由该线程使用,不会与其他线程共享。堆是进程的属性,任何线程都可以使用它。堆是一个共享内存空间,其中一个线程的数据也可以被其他线程访问。
现在我们得到了进程和线程的一般概念。但它们的用途是什么?
当您启动 Web 浏览器时,一定会有一些代码指示操作系统执行某些操作。这意味着我们正在创建一个进程。该进程可能要求操作系统为新选项卡创建另一个进程。当浏览器选项卡打开并且您正在处理日常的事情时,该选项卡进程将开始为不同的活动创建不同的线程(如页面滚动,下载,听音乐等),就像我们在前面的图表中看到的那样。
以下是 iOS 平台上 Chrome 浏览器应用程序的截图。
![](https://raw.githubusercontent.com/studygolang/gctt-images/master/acheiving-concurrency-in-go/3.png)
截图显示,谷歌 Chrome 浏览器正在为打开的标签和内部服务使用不同的进程。由于每个进程至少有一个线程,我们可以看到,在这种情况下,Google Chrome 进程有超过 10 个线程。
在之前的主题中,我们谈到了处理多件事或做多件事。这里的一件事是由线程执行的活动。因此,当并发或并行模式中发生多个事情时,有多个线程以串行或并行方式运行,即多线程。
> 在多线程中,在进程中产生的多个线程,具有内存泄漏的线程可能耗尽其他线程的资源并使进程无响应。在使用浏览器或任何其他程序时,您可能已经多次看到过这种情况。您可能已经使用活动监视器或任务管理器来查看无响应的进程并将其终止。
## 线程调度
当多个线程串行或并行运行时,由于多个线程可能共享某些数据,因此线程需要协调工作,以便一次只能有一个线程访问特定数据。 以某种顺序执行多个线程称为调度。Os 线程由内核调度,一些线程由编程语言的运行时环境管理,如 JRE 。当多个线程试图同时访问相同数据导致数据被更改或导致意外结果时,则会出现竞争条件。
> 在设计并发计划时,我们需要查看在后面的课程中讨论的竞争条件。
![](https://raw.githubusercontent.com/studygolang/gctt-images/master/acheiving-concurrency-in-go/4.png)
## Go 中的并发性
最后,我们谈到了如何实现并发性。像 `java` 这样的传统语言有一个线程类,可用于在当前进程中创建多个线程。由于 `go` 没有传统的 OOP 语法,因此它提供了 `go` 关键字来创建 `goroutines`。当 `go` 关键字放在函数调用之前时,它就变成了 `goroutines`。
我们将在下一课中谈论 Goroutines,但简而言之,goroutines 表现得像线程,但在技术上它其实是对线程的抽象。
当我们运行 Go 程序时,**运行时**将在核心上创建几个线程,其中所有 Goroutine 都被多路复用(*催生*)。在任何时间点,一个线程将执行一个 Goroutine,如果该 Goroutine 被阻塞,那么它将被替换为另一个将在该线程上执行的 Goroutine。这就像**线程调度**,但由**运行时处理**,这要快得多。
在大多数情况下,建议在一个核心上运行所有 Goroutine,但如果需要在系统的可用 CPU 核心之间划分 Goroutine,则可以使用 GOMAXPROCS 环境变量或使用函数 `runtime.GOMAXPROCS(n)` 调用运行时。其中 `n` 是要使用的核心数。但您可能有时会觉得设置 `GOMAXPROCS> 1` 会使您的程序变慢。它真正取决于您的程序的性质,但您可以在互联网上找到您的问题的解决方案或解释。实际上,在使用多个内核,操作系统线程和进程时,如果花费更多时间在通道上进行通信而不是使用进行计算的程序将会出现性能下降。
Go 有一个 `M:N` 调度程序,也可以使用多个处理器。在任何时候,`M` 个 Goroutine 都需要在最多运行在 `GOMAXPROCS` 处理器数量上的 `N` 个 OS 线程上进行调度。在任何时候,每个核心最多只允许一个线程运行。但是调度程序可以根据需要创建更多线程,但很少发生。如果你的程序没有启动任何额外的 Goroutine,它将自然只在一个线程中运行,无论你允许它使用多少个核心。
## 线程与 Goroutines
正如我们之前看到的那样,线程和 Goroutines 之间存在明显的差异,但是下面的差异将阐明为什么线程比 Goroutines 更昂贵,以及为什么 Goroutines 是在应用程序中实现最高并发性的关键性解决方案。
| 线程 | Goroutine |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| Os 线程由内核管理,并具有硬件依赖性。 | Goroutine 由 Go 运行时管理,没有硬件依赖性。 |
| Os 线程通常具有 1-2 MB 的固定堆栈大小 | Goroutines 通常在较新版本的 Go 中具有 8KB 的堆栈大小 |
| 堆栈大小在编译期间确定,不能增长 | Go 的堆栈大小在运行时进行管理,并且可以通过分配和释放堆存储来增长到 1GB |
| 线程之间没有简单的通信媒介。线程间通信之间存在巨大的延迟。 | Goroutine 使用 channel 与其他的低延迟 Goroutine 进行通信([阅读更多](https://blog.twitch.tv/gos-march-to-low-latency-gc-a6fa96f06eb7))。 |
| 线程有标识。有一个 TID 标识进程中的每个线程。 | Goroutine 没有任何标识。go 实现了这个是因为 Go 没有 TLS([线程本地存储](https://docs.microsoft.com/zh-cn/windows/desktop/ProcThread/thread-local-storage))。 |
| 线程具有明显的启动和销毁成本,因为线程必须从 Os 请求大量资源并在完成后返回。 | Goroutines 由 Go 的运行时创建和释放。与线程相比,这些操作非常简便,因为运行时已经 Goroutine 维护了线程池。在这种情况下,Os 不知道 Goroutines。 |
| 线程被预先安排([阅读更多](https://stackoverflow.com/questions/4147221/preemptive-threads-vs-non-preemptive-threads))。由于调度程序需要保存 / 恢复 50 个以上的寄存器和状态,因此线程之间的切换成本很高。当线程之间需要快速切换时,这可能非常重要。 | Goroutines 是合作安排的([阅读更多](https://stackoverflow.com/questions/37469995/goroutines-are-cooperatively-scheduled-does-that-mean-that-goroutines-that-don))。当发生 Goroutine 切换时,只需要保存或恢复 3 个寄存器就可以了。 |
以上是一些重要的差异,但如果你深入了解,你会发现 Go 的并发模型的惊人之处。为了突出 Go 的并发强度的一些功能点,假设您有一个 Web 服务器,您每分钟处理 1000 个请求。如果必须同时运行每个请求,这意味着您需要创建 1000 个 线程或在不同的进程下划分它们。这就是 Apache 服务器管理传入请求的方式([阅读更多](https://httpd.apache.org/docs/2.4/mod/worker.html))。如果一个 Os 线程中每个线程消耗 1MB 的堆栈大小,这意味着你将会为该流量耗尽 1GB 的 RAM。Apache 提供 `ThreadStackSize` 指令来管理每个线程的堆栈大小,但是,您仍然不知道是否会因为这个问题而遇到新问题。
对于 Goroutines,由于堆栈大小可以动态增长,因此可以毫无问题地生成 1000 个 Goroutine。由于 Goroutine 以 8KB 的堆栈空间开始,因此大多数堆栈空间通常不会变大。但是如果存在需要更多内存的递归操作,那么可以将堆栈大小增加到 1GB,除了 `for{}` 这种几乎是一个 bug 之外,我认为几乎不会发生这种情况。
与我们之前看到的线程相比,goroutine 之间的快速切换也是可能的并且更有效。由于一个 Goroutine 一次在一个线程上运行并且 Goroutine 是协同安排的,因此在当前 Goroutine 被阻止之前不会安排另一个 Goroutine。如果该线程中的任何 Goroutine 被阻塞说等待用户输入,则会在其位置安排另一个 Goroutine。goroutine 可能会因为以下几种情况阻塞。
- 网络的输入
- sleeping
- channel 操作
- 在 sync 包中的锁的基本操作
如果 Goroutine 没有在这些条件之一上阻塞,它就会使它所在的多线程中断,在这个过程中杀死其他的 Goroutine。虽然有一些补救措施,但如果确实如此,那么它会被认为是糟糕的编程。
`Channels` 在与 Goroutines 一起作为媒介分享他们之间的数据时扮演着重要的角色,我们会在后面的课程中学习。这会防止竞争条件和它们之间对共享数据的不适当访问,而不是在线程的情况下对共享内存的访问。
## 更多资源
有一篇关于 Go 调度程序的文章,名字叫 Go 的工作窃取调度器 [<https://github.com/rakyll>] 您应该阅读以了解 Go 的运行时如何管理 Goroutines。
Rob Pike 对 Golang 的并发性有一个很棒的演讲,题目是 [“并发不是并行”](https://www.youtube.com/watch?v=cN_DpYBzKso) 。
既然我们了解了 Goroutines 是什么以及它们如何在幕后工作,那么让我们深入了解下一课,我们将学习如何创建一个 Goroutine 并在我们的 Go 计划中使用它们。goroutines 是分配许多工人的工作量以使工作轻松快捷的重要媒介。这就是为什么 Go 是一种完美的编程语言,可以为您的下一个应用程序提供微系统架构。
via: https://medium.com/rungo/achieving-concurrency-in-go-3f84cbf870ca
作者:Uday Hiwarale 译者:wumansgy 校对:alfred-zhong
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
有疑问加站长微信联系(非本文作者))