Code前端首页关于Code前端联系我们

Golang调度器怎么样

terry 2年前 (2023-09-24) 阅读数 62 #后端开发
Go语言在开发者中非常流行。 Go语言拥有极其简单的部署方式(可以直接编译机器码,除了C标准外,操作系统库几乎不依赖任何系统库,直接运行即可部署)、优秀的编译速度,” “遗传”级别的并发支持、强大的标准库支持、低廉的开发成本、简单易学的平台特性将给每一个接触过的后端开发者留下深刻的印象。 Docker 的崛起以及 Kubernetes 第二波浪潮的影响,也让 Go 在后端取代了 Go,尤其是在中高层企业需求(对性能、代码质量、架构设计、等)。摇。后端开发者逐渐开始对Go语言感到惊讶。无论你是擅长任何语言的后端开发人员,你都想学习Golang。 所有软件都在操作系统上运行,因此使软件运行和发挥作用的是实际用于计算的处理器。在早期的操作系统中,每个程序都是一个进程。在一个程序运行完毕之前,另一进程无法继续。这是单进程时代,所有程序只能串行发生,如图1.1所示。 Golang 调度器到底怎么回事图1.1 单进程时代的操作系统1。单进程时代不需要调度器早期的单进程操作系统面临两个问题。

(1)简单的执行过程。一台计算机一次只能处理一项任务,所有程序都几乎陷入僵局,更不用说处理图形界面或鼠标等异步交互的能力了。

(2) 进程阻塞导致的 CPU 时间损失。在进程的完整生命周期中,要访问的物理部分包括CPU、缓存、主存、磁盘、网络等。不同硬件介质的计算能力差异很大。如果这些具有不同处理速度的过程介质通过一个过程链接起来,则高速介质将等待并丢失。例如,当程序从磁盘读取数据时,CPU在读写过程中处于等待状态。当然,在单进程操作系统的情况下,这会导致CPU处理能力的浪费,因为CPU本身应该充分分配给其他进程来执行高级计算。 那么是否可以有多个进程一起执行多个宏观层面的任务呢?后来操作系统有了最早的并发能力,多进程并发。当一个进程阻塞时,切换到另一个进程等待执行,这样可以尽可能地利用CPU,不浪费CPU。2。多进程/线程时代的调度器要求 多进程/多线程操作系统解决了阻塞问题。如果某个进程阻塞了CPU,可以立即切换到其他进程执行并进行CPU调度算法。所有正在运行的进程都保证被分配到一个CPU时隙。从宏观上看,表现为多个进程同时运行,如图1.2所示。 Golang 调度器到底怎么回事图1.2 多线程/多进程操作系统 图1.2展示了CPU通过调度器切换CPU时间线的场景。将来如果每个进程/线程宏观上一起执行,就必须切换CPU,给每个进程分配一个时隙。 但是新的问题又出现了。该进程拥有太多资源。创建、切换和销毁进程都会花费很长时间。即使CPU正在使用,如果进程太多,CPU也会受到很大的影响。该部分用于调度进程切换,如图1.3所示。 Golang 调度器到底怎么回事图1.3切换CPU调度的成本在Linux操作系统中,CPU对于进程和线程的态度是一样的。如图1.3所示,如果系统中CPU数量过少,进程/线程数量过少。这部分功耗实际上并没有用到程序有用的计算能力上,所以虽然线程看起来很漂亮,但实际上多线程的开发设计就变得复杂了,开发者不得不考虑很多同步问题。 ,比如锁、资源争用、同步冲突等。 3。协程提高CPU利用率那么我们如何提高CPU利用率呢?多处理和多线程提高了系统的并发能力,但在当今高并发的互联网场景下,为每个任务创建一个线程是不现实的,因为这会导致大量线程同时运行。时间,不仅切换到High,而且还消耗大量内存(进程的虚拟内存将占用4GB[32位操作系统],线程也将占用4MB左右)。大量进程或线程会出现新问题。

(1)高内存消耗。

(2) 调度期间 CPU 消耗较高。

工程师发现,线程实际上可以分为两种形式的线程:“内核态”和“用户态”。所谓用户态线程只是为了在用户态实现内核态线程。目的是更轻量(更少的内存使用,更少的隔离,更快的调度)和更可控(你可以自己做,控制调度器)。用户模式下的所有内容对于内核模式都是可见的,但对于内核来说用户模式线程只是内存中的一堆数据。用户态线程必须与内核态线程绑定,但CPU并不知道用户态线程的存在。它只知道一个线程运行在内核态(PCB Linux进程控制块),如图1.4所示。 Golang 调度器到底怎么回事图1.4 线程中的用户态和内核态

如果将线程进一步分类,内核线程仍然称为线程(Thread),用户线程称为协程(Co-routine)。操作系统级线程就是所谓的内核态线程,而用户态线程则多种多样,它们可以在同一个内核线程上执行多个任务,例如Coroutine、Golang的Goroutine、C#的Task等。

既然一个协程可以绑定一个线程,那么多个协程是否可以绑定一个或者多个线程呢?此外,协程和线程之间存在三种映射关系。它们是N:1关系、1:1关系和M:N关系。 (1) N: 1 关系 N 个协程绑定到一个线程。优点是协程在用户态线程中完成切换,不会陷入内核态。该开关非常轻且快速。 ,但缺点也很明显。所有进程协程都绑定到单个线程,如图 1.5 所示。 Golang 调度器到底怎么回事图 1.5 协程和线程的 N:1 关系

N:1 关系面临几个问题:

(1) 某个程序无法使用硬件的多核加速能力 2) A某个程序如果协程被阻塞,那么该线程就会被阻塞,该进程的其他协程将无法执行,导致没有并发能力。 (2) 1:1关系 1个协程绑定1个线程,这是最容易实现的。协程调度由CPU完成。虽然 N:1 没有缺点,但是创建、删除和切换协程都是由 CPU 完成,成本稍高。协程和线程之间1:1的关系如图1.6所示。 Golang 调度器到底怎么回事图 1.6 协程与线程 1:1 关系 (3) M:N 关系 M 协程绑定 N 个线程,是 N:1 和 1:1 两种类型的组合,克服了以上两种模型,但实现起来最复杂,如图1.7所示。 M个协程挂在同一个调度器上,调度器后面有多个CPU核心资源。协程和线程之间是有区别的。线程在CPU上以抢占的方式调度,而协程在用户态以协作的方式调度。只有将一个协程让给CPU后,才能执行另一个协程,因此对于M:N模型中间层调度器的设计就变得尤为重要。提高线程和协程的耦合度和效率也成为不同语言设计调度器的优先考虑事项。Golang 调度器到底怎么回事图1.7 协程和线程之间的M:N关系4. Go 中的协程 GoroutineGo 使用 Goroutine 和 Channel 来提供更易于使用的并发方法。 Goroutine 来自协程的概念,它允许一组可重用的函数在一组线程上运行。即使协程阻塞,线程的其他协程也可以由运行时调度并转移到其他可执行线程。最关键的是,程序员看不到这些底层细节,这降低了编程复杂性并提供了更容易的并发性。

在Go中,协程被称为Goroutine。它很轻。一个 Goroutine 只需要几 KB,而这几 KB 足以运行该 Goroutine。它可以在有限的内存空间内支持大量的Goroutine。支持更大的并发性。虽然Goroutine堆栈只占用几KB,但它实际上是可扩展的。如果需要额外的内容,运行时会自动将其分配到 Goroutine 区域。

Goroutine 的特点是更小的内存占用(几KB)和更灵活的调度(运行时调度)。 5。废弃的Goroutine调度器现在我们知道了协程和线程的关系,最重要的一点就是实现一个调度器来调度协程。 Go 目前使用的调度器是在 2012 年重新设计的。由于之前的调度器存在性能问题,使用了 4 年后就被放弃了。那么首先我们来分析一下废弃的调度器是如何工作的?通常,符号 G 用于表示 Goroutine,M 用于表示 Thread,如图 1.8 所示。下面关于调度器的内容统一使用图1.8所示的符号来表示。 Golang 调度器到底怎么回事 图 1.8 G 和 M 的符号表示 我们来看看废弃的 Golang 调度器是如何实现的?如图1.9所示,早期的调度器是基于M:N实现的。图 1.9 是一个概要图。所有的协程,也就是我们的G,都会被纳入到Go协程的全局秩序中。由于全局队列外共享多个M资源,因此会增加夹克同步。和互斥锁。 Golang 调度器到底怎么回事图1.9 Golang早期调度器的处理M如果想要运行它或者返回给G就必须访问全局队列G,并且有多个M,即多个线程必须访问锁在其中的同一个资源为了提供互斥/同步,因此全局队列G受到互斥锁的保护。

不难分析,老规划师有几个缺点。

(1) G 的创建、销毁和规划都需要每个 M 获取锁,造成激烈的锁竞争。

(2)M 传输 G 将导致系统延迟和额外负载。例如,当 G 包含新协程的创建时,M 创建 G'。为了继续执行G,G'需要传递给M2(如果已分配)执行,这也会导致不好的局部性,因为G'引用G并且最好在M上执行而不是在其他M2上执行,如图所示1.10. Golang 调度器到底怎么回事 图1.10 早期Golang调度器的局部性问题

(3) 系统调用(在M之间切换CPU)导致频繁的线程阻塞和解阻塞操作,从而增加了系统开销。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门