如果我必须选择Go的一个很棒的功能,那么它必须是内置的并发模型。它不仅支持并发性,而且使其更好。将并发模型(goroutines)转换为并发性是Docker对虚拟化的要求。
什么是并发?
在计算机编程中,并发性是计算机同时处理多个事物的能力。例如,如果你在浏览器中上网,可能会发生很多事情。在特定情况下,你可能会同时在正在滚动的页面上收听某些音乐时下载某些文件。因此,浏览器需要立即处理很多事情。如果浏览器无法立即处理它们,你需要等到所有下载完成后再开始浏览互联网。那会令人沮丧。
通用PC可能只有一个CPU核心,可以完成所有处理和计算。CPU核心可以一次处理一件事。当我们谈论并发时,我们一次只做一件事,但我们将CPU时间划分为需要处理的事物。因此,在现实中,我们同时感受到多件事情的发生,一次只发生一件事。
让我们看一下CPU管理Web浏览器如何管理我们所讨论的示例中的内容。
因此,从上图中,你可以看到单个核心处理器几乎根据每个任务的优先级划分工作量,例如,页面滚动,听音乐可能具有低优先级,因此有时你的音乐因互联网速度慢而停止,但你仍然可以滚动页面。
什么是并行性?
但问题出现了,如果我的CPU有多个内核怎么样?如果处理器有多个处理器,那么它称为多核处理器。你可能在购买笔记本电脑,个人电脑或智能手机时听说过这个术语。多核处理器能够同时处理多个事物。
在之前的Web浏览示例中,我们的单核处理器必须在不同的事物之间划分CPU时间。使用多核处理器,我们可以在不同的核中同时运行单独的东西。让我们用下图来评估它。
并行运行不同的东西的概念称为并行。当我们的CPU有多个内核时,我们可以使用不同的CPU内核同时执行多个操作。因此,我们可以说我们可以很快完成一项工作(包括许多事情),但事实并非如此。我会回到这一点。
并发 vs 并行
Go建议仅在一个核上使用gorotuines,但我们可以修改go程序以在不同的处理器核上运行goroutine。现在,将goroutines视为功能,因为它们是,但还有更多功能。
并发和并行之间存在一些差异。虽然并发性同时处理(dealing with)多个事物,但并行性却同时做(doing)多个事物。并行性并不总是有利于并发,我们将在即将到来的课程中学到这一点。
在这一点上,可能有很多问题在脑海中浮现,你可能已经有了并发的想法,但你可能想知道如何实现它以及如何使用它。要了解go的并发体系结构以及如何在代码中使用它,以及何时在应用程序体系结构中使用它,我们需要了解计算机进程是什么。
什么是计算机程序?
当你使用C,java或go等语言编写计算机程序时,它只是一个文本文件。但是,由于你的计算机只能理解由0和1组成的二进制指令,因此你需要将该代码编译为机器语言。这就是编译器的用武之地。在python和javascript等脚本语言中,解释器也会做同样的事情。
当编译程序被发送到OS来处理时,OS分配不同的东西,如内存地址空间(进程的堆和堆栈将位于何处),程序计数器,PID(进程ID)和其他非常关键的东西。进程至少有一个线程称为主线程,而主线程可以创建多个其他线程。当主线程完成它的执行时,进程退出。
所以我们理解该进程是一个容器,它具有已编译的代码,内存,不同的OS资源以及可以提供给线程的其他东西。简而言之,进程是内存中的一个程序。但是什么是线程,他们的工作是什么?
什么是线程?
线程是进程内的轻量级进程。线程是一段代码的实际执行者。线程可以访问进程提供的内存,OS资源句柄和其他内容。
在执行代码时,线程将变量(数据)存储在内存区域内,称为栈,其中包含变量占用临时空间的空间。栈是在编译时创建的,通常是固定大小,最好是1-2 MB。线程的栈只能由该线程使用,不会与其他线程共享。堆是进程的属性,任何线程都可以使用它。堆是一个共享内存空间,其中一个线程的数据也可以被其他线程访问。
现在我们得到了进程和线程的一般概念。但它们的用途是什么?
当你启动Web浏览器时,必须有一些代码指示操作系统执行某些操作。这意味着我们正在创建一个进程。该过程可能要求操作系统为新选项卡创建另一个进程。当浏览器选项卡打开并且你正在处理正常的日常事务时,该选项卡进程将开始为不同的活动创建不同的线程(如页面滚动,下载,听音乐等),如之前的图表所示。
以下是iOS平台上Chrome浏览器应用程序的屏幕抓取。
屏幕抓取显示,谷歌Chrome浏览器正在为打开的标签和内部服务使用不同的进程。由于每个进程至少有一个线程,我们可以看到,在这种情况下,Google Chrome进程有超过10个线程。
在之前的主题中,我们谈到了处理多件事或做多件事。这里的一件事是由线程执行的活动。因此,当并发或并行模式中发生多个事情时,有多个线程以串行或并行方式运行,即AKA多线程。
在多线程中,在进程中产生多个线程,具有内存泄漏的线程可能耗尽其他线程的资源并使进程无响应。使用浏览器或任何其他程序时,你可能已经多次看到过这种情况。你可能已经使用活动监视器或任务管理器来查看无响应的进程并将其终止。
线程调度
当多个线程串行或并行运行时,由于多个线程可能共享某些数据,因此线程需要协调工作,以便一次只能有一个线程访问特定数据。以某种顺序执行多个线程称为调度。Os线程由内核调度,一些线程由编程语言的运行时环境管理,如JRE。当多个线程试图同时访问相同数据导致数据被更改或导致意外结果时,则会出现竞争条件。
在设计并发go程序时,我们需要寻找将在即将到来的课程中讨论的竞争条件。
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,它将自然只在一个线程中运行,无论你允许它使用多少个核。
线程 vs goroutine
正如我们之前看到的那样,线程和goroutines之间存在明显的差异,但是下面的差异将阐明为什么线程比goroutines更昂贵,以及为什么goroutines是在应用程序中实现最高并发性的关键解决方案。
以上是一些重要的差异,但如果你深入了解,你会发现go的并发模型的惊人世界。为了突出go的并发强度的一些功能点,假设你有一个Web服务器,你每分钟处理1000个请求。如果必须同时运行每个请求,这意味着你需要创建1000个线程或在不同的进程下划分它们。这就是Apache服务器管理传入请求的方式(在此处阅读)。如果一个Os线程每个线程消耗1MB的栈大小,这意味着你将为该流量耗尽1GB的RAM。Apache提供ThreadStackSize指令来管理每个线程的栈大小,但是,你仍然不知道是否因为这个问题而遇到问题。
对于goroutines,由于栈大小可以动态增长,因此可以毫无问题地生成1000个goroutine。由于goroutine以8KB的栈空间开始,因此大多数栈空间通常不会变大。但是如果存在需要更多内存的递归操作,那么可以将栈大小增加到1GB,除了 for {} 这几乎是一个bug之外,我几乎认为不会发生这种情况。
与我们之前看到的线程相比,goroutine之间的快速切换也是可能的并且更有效。由于一个goroutine一次在一个线程上运行并且goroutine是协同安排的,因此在当前goroutine被阻止之前不会安排另一个goroutine。如果该线程中的任何Goroutine阻止说等待用户输入,则在其位置安排另一个goroutine。goroutine可以阻止以下某种情况:
*network input
*sleeping
*channel operation
*阻塞,在同步包中的原语
如果goroutine没有阻止其中一个条件,它可能会使多路复用的线程饿死,从而杀死其他goroutine。虽然有一些补救措施,但如果确实如此,那么它被认为是糟糕的编程。
在与goroutines一起作为媒介分享他们之间的数据时,channel将发挥重要作用,我们将在即将到来的课程中学习。这将防止竞争条件和对它们之间的共享数据的不适当访问,而不是在线程的情况下访问共享内存。