Golang 并发之一 ( go并发模型)

如果必须选择 Go 的一项伟大功能,那么它必须是内置的并发模型。它不仅支持并发,而且使它变得更好。 Go Concurrency Model (goroutines) 之于并发,就像 Docker 之于虚拟化。

什么是并发(Concurrency)?

在计算机编程中,并发(Concurrency)是计算机同时处理多个事物的能力。例如,如果您在浏览器中上网,可能会同时发生很多事情。在特定情况下,您可能正在下载一些文件,同时在您滚动的页面上听一些音乐。因此浏览器需要同时处理很多事情。如果浏览器无法立即处理它们,您需要等待所有下载完成,然后您才能再次开始浏览互联网。那会令人沮丧。

一般来说 PC 可能只有一个 CPU 内核来完成所有的处理和计算。一个 CPU 内核同一时间只能处理一件事。当我们谈论并发(concurrency)时,我们一次只做一件事,但可以将 CPU 时间分配给需要处理的事情。因此,我们会感觉同时发生了多种事情,但事实上一次只发生了一件事情。

我们通过图表来了解上述讨论的案例: CPU 通过 Web 浏览器如何"同时"处理多个事情。

cpu 并发模型

所以从上图可以看出,单核处理器几乎是根据每个任务的优先级来划分工作负载的,例如,在页面滚动时,听音乐的优先级可能较低,因此有时您的音乐会因低优先级而停止互联网速度,但您仍然可以滚动页面。

什么是并行(parallelism)?

但是问题来了,如果 CPU 有多个内核呢?如果一个处理器有多个处理器,则称为多核处理器。多核处理器能够同时处理多项事情。 在之前的网页浏览示例中,我们的单核处理器必须在不同的事物之间分配 CPU 时间。使用多核处理器,我们可以在不同的内核中同时运行不同的东西。让我们使用下图来评估。

cpu 并行模型

并行运行不同事物的概念称为并行(parallelism)性。当我们的 CPU 有多个内核时,我们可以使用不同的 CPU 内核同时做多事情。因此,我们可以说我们可以很快完成一项工作(包括很多东西),但事实并非如此。我们后面在讨论这一点。

并发 (concurrency) vs 并行 (parallelism)

Go 建议只在一个内核上使用 goroutines,但我们可以修改 Go 程序以在不同的处理器内核上运行 goroutines。现在,将 goroutines 视为 Go 函数,因为它们就是,但还有更多。

并发性和并行性之间有几个区别。并发一个人在同一时间处理多个事情,多个事情之间回切换;而并行是多人同时处理多个事情,每个人处理其中的一件。但并行并不总是比并发更有利,我们在下一片文章来讨论这个问题。

此时,您的脑海中可能会有很多问题,您可能已经有了并发的想法,但您可能想知道 Go 如何实现它以及如何使用它。要了解 Go 的并发架构以及如何在代码中使用它,以及何时在应用程序中使用它,我们需要了解什么是计算机进程。

计算机进程(process)是什么?

当您使用 C、java 或 Go 等语言编写计算机程序时,它只是一个文本文件。但是由于计算机只能理解由 0 和 1 组成的二进制指令,因此需要将该代码编译为机器语言。这就是编译器的用武之地。在 python 和 javascript 等脚本语言中,解释器做同样的事情。

当一个编译好的程序被送到操作系统(OS) 处理时,操作系统(os) 会分配不同的东西,比如内存地址空间(进程的堆和栈所在的位置)、程序计数器、PID(进程 ID)和其他非常重要的东西。一个进程至少有一个线程称为主线程,而主线程可以创建多个其他线程。当主线程执行完毕后,进程退出。

所以我们理解进程是一个容器,它已经编译了代码、内存、不同的操作系统资源和其他可以提供给线程的东西。简而言之,进程就是内存中的一个程序。但是什么是线程,它们的工作是什么?

什么是线程(thread)?

线程是进程内的轻量级进程。线程是一段代码的实际执行者。线程可以访问进程提供的内存、操作系统资源和其他东西。

在执行代码时,线程在内存区域内存储变量(数据)称为堆栈,其中临时空间变量保存临时空间。堆栈在运行时创建,通常具有固定大小,最好为 1-2 MB。而一个线程的堆栈只能由该线程使用,不会与其他线程共享。堆是进程的一个属性,可供任何线程使用。堆是一个共享内存空间,来自一个线程的数据也可以被其他线程访问。

现在我们大致了解了进程和线程。但是它们有什么用呢?

当启动 Web 浏览器时,必须有一些代码指示操作系统执行某些操作。这意味着我们正在创建一个进程。该进程可能会要求操作系统为新选项卡创建另一个进程。当浏览器选项卡打开并且同时您正在做日常工作,该选项卡进程将开始为不同的活动(如页面滚动、下载、听音乐等)创建不同的线程,正如我们在之前的图表中看到的那样。

下面是 macOS 平台上 Chrome 浏览器应用程序的屏幕截图

process on mac

上面的屏幕截图显示 Google Chrome 浏览器对打开的标签页和内部服务使用不同的进程。由于每个进程至少有一个线程,我们可以看到一个谷歌浏览器进程,在这种情况下,有超过 3 个线程。

在之前的话题中,我们谈到了处理多件事或做多件事。这里的事物是由线程执行的活动。因此,当在并发或并行模式下发生多件事情时,会有多个线程串联或并行运行,也就是多线程。

在多线程中,在一个进程中产生多个线程,内存泄漏的线程会耗尽其他线程的资源并使进程无响应。在使用浏览器或任何其他程序时,您可能已经多次看到这种情况。您可能已经使用活动监视器或任务管理器来查看无响应的进程并杀死它。

线程调度

当多个线程串行或并行运行时,由于多个线程可能共享一些数据,因此线程需要协同工作,以便一次只有一个线程可以访问特定数据。以某种顺序执行多个线程称为调度。操作系统线程由内核调度,一些线程由编程语言的运行时环境管理,如 JRE。当多个线程试图同时访问相同的数据导致数据被更改或导致意外结果时,就会发生竞争条件。

在设计并发 Go 程序时,我们需要注意竞争条件,我们将在下一片文章中讨论。

single thread vs multi thread

Go 中的并发(concuenry)

最后,我们将讨论 Go 如何实现并发。像java这样的传统语言有一个线程类,可以用来在当前进程中创建多个线程。由于 Go 没有传统的 OOP 语法,它提供了 go 关键字来创建 goroutine。当 go 关键字放在函数调用之前,它就变成了 goroutines。

我们将在下一片文章中讨论 goroutines,但简而言之,goroutines 的行为类似于线程,但在技术上;它是对线程的抽象。

当我们运行 Go 程序时,Go runtime 将在一个核心上创建几个线程,所有 goroutine 都在该核心上复用(产生)。在任何时候,一个线程将执行一个 goroutine,如果该 goroutine 被阻塞,那么它将被替换为另一个将在该线程上执行的 goroutine。这就像线程调度,但由 Go runtime 处理,而且速度要快得多。

在大多数情况下,建议在一个内核上运行所有 goroutines,但是如果您需要在系统的可用 CPU 内核之间划分 goroutines,您可以使用 GOMAXPROCS 环境变量或使用函数 runtime.GOMAXPROCS(n) 调用运行时其中 n 是要使用的内核数。但是有时您可能会觉得设置 GOMAXPROCS > 1 会使您的程序变慢。这确实取决于程序的性质,但您可以在互联网上找到问题的解决方案或解释。实际上,当程序使用多核、操作系统线程和进程时,在通道上通信比在计算上花费更多时间的程序会遇到性能下降。

Go 有一个 M:N 调度器,它也可以使用多个处理器。在任何时候,都需要在 N 个操作系统线程上调度 M 个 goroutine,这些线程最多在 GOMAXPROCS 个处理器上运行。在任何时候,每个内核最多只能运行一个线程。但是调度程序可以根据需要创建更多线程,但这很少发生。如果你的程序没有启动任何额外的 goroutines,那么无论你允许它使用多少个内核,它自然只会在一个线程中运行。

线程(threads) vs goroutines

正如我们之前看到的,线程和 goroutines 之间存在明显的区别,但下面的区别将阐明为什么线程比 goroutines 更昂贵,以及为什么 goroutines 是实现应用程序中最高级别并发的关键解决方案。

thread goroutines
操作系统线程由内核管理并具有硬件依赖性。 goroutines 由 go runtime管理,没有硬件依赖。
OS 线程通常具有 1-2MB 的固定堆栈大小 在较新版本的 go 中,goroutines 通常具有 8KB(自 Go 1.4 以来为 2KB)的堆栈大小
堆栈大小在编译时确定,不能增长 go 的堆栈大小在运行时进行管理,可以通过分配和释放堆存储增加到 1GB
线程之间没有简单的通信媒介。线程间通信之间存在巨大的延迟。 goroutine 使用通道以低延迟与其他 goroutine 通信
线程具有标识。有 TID 标识进程中的每个线程。 goroutine 没有任何身份。 go 实现了这一点,因为 go 没有 TLS(线程本地存储Thread Local Storage)。
线程具有显着的设置和拆卸成本,因为线程必须从操作系统请求大量资源并在完成后返回 goroutine 由 go runtime 创建和销毁。与线程相比,这些操作非常便宜,因为 go runtime 已经为 goroutine 维护了线程池。在这种情况下,操作系统不知道 goroutines。
线程是预先调度的。由于调度程序需要保存/恢复超过 50 个寄存器和状态,因此线程之间的切换成本很高。当线程之间快速切换时,这可能非常重要。 goroutine 是协同调度的。当发生 goroutine 切换时,只需要保存或恢复 3 个寄存器。

以上是一些重要的区别,但如果你深入研究,你会发现 Go 并发模型的惊人世界。为了突出 Go 并发强度的一些优势,假设您有一个 Web 服务器,每分钟处理 1000 个请求。如果您必须同时运行每个请求,则意味着您需要创建 1000 个线程或将它们划分到不同的进程中。这就是 Apache 服务器管理传入请求的方式。如果 OS 线程每个线程消耗 1MB 堆栈大小,则意味着您将耗尽 1GB RAM 用于该流量。 Apache 提供了 ThreadStackSize 指令来管理每个线程的堆栈大小,但您仍然不知道是否因此而遇到问题。

而在 goroutine 的情况下,由于堆栈大小可以动态增长,您可以毫无问题地生成 1000 个 goroutine。由于 goroutine 以 8KB(自 Go 1.4 以来为 2KB)的堆栈空间开始,它们中的大多数通常不会增长得比这更大。但是,如果存在需要更多内存的递归操作,Go 可以将堆栈大小增加到 1GB,我认为这几乎不会发生,除了 for {}, 这显然是一个错误。

此外,与我们之前看到的线程相比,goroutines 之间的快速切换是可能的并且更高效。由于一个 goroutine 一次在一个线程上运行并且 goroutine 是协作调度的,因此在当前 goroutine 被阻塞之前不会调度另一个 goroutine。如果该线程块中的任何 Goroutine 说等待用户输入,那么另一个 goroutine 将被安排在它的位置。

goroutine 可以在遇到以下条件之一时阻塞

  • network input
  • sleeping
  • channel operation
  • blocking on primitives in the sync package

如果 goroutine 没有在这些条件之一上阻塞,它可以使多路复用的线程饿死,杀死进程中的其他 goroutine。虽然有一些补救措施,但如果确实如此,那么它就被认为是糟糕的编程。

Channels 在goroutine 间共享数据时,将发挥重要作用,下一章我们将讨论这个问题。这将防止竞争条件和对共享数据的不当访问,而不应在多线程的情况下访问共享内存。

更多参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 1. C/C++ 与 Go语言的“价值观”对照 C的价值观摘录 相信程序员:提供指针和指针运算,让C程序员天马行空...
    ywhu阅读 6,894评论 0 13
  • 正文开始之前先抛出一个思考:让一个静态网站满足海量用户访问本质上是一个并行问题还是并发问题? 并发的世界 并发这个...
    谢培阳阅读 1,969评论 3 16
  • 控制并发有三种种经典的方式,一种是通过channel通知实现并发控制 一种是WaitGroup,另外一种就是Con...
    wiseAaron阅读 10,648评论 4 34
  • Go 并发编程 选择 Go 编程的原因可能是看中它简单且强大,那么你其实可以选择C语言;除此之外,我看中 Go 的...
    PRE_ZHY阅读 881评论 1 6
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,529评论 28 53