并发

2023-11-30 23:27:00 浏览数 (2)

进程和线程

A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发和并行

A. 多线程程序在一个核的cpu上运行,就是并发。

B. 多线程程序在多个核的cpu上运行,就是并行。

并发是指逻辑上具备同时处理多个任务的能力;并行则是物理上同时执行多个任务。

协程和线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。创建一个 goroutine 的栈内存消耗为 2-4 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存,而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。Thread 创建和销毀都会有巨大的消耗,因为要和操作系统打交道,是内核级的,通常解决的办法就是线程池。而 goroutine 因为是由 Go runtime 负责管理的,创建和销毁的消耗非常小,是用户级。

线程的时间成本主要来自于切换线程上下文时,用户态与内核态的切换、线程的调度、寄存器变量以及状态信息的存储。

如果两个线程位于不同的进程,进程之间的上下文切换还会因为内存地址空间的切换导致缓存失效,所以不同进程的切换要显著慢于同一进程中线程的切换(现代的 CPU 使用快速上下文切换技术解决了进程切换带来的缓存失效问题)

线程的空间成本主要来自于线程的堆栈大小。线程的堆栈大小一般是在创建时指定的,为了避免出现栈溢出(Stack Overflow),默认的栈会相对较大(例如 2MB),这意味着每创建 1000 个线程就需要消耗 2GB 的虚拟内存,这大大限制了创建的线程的数量(虽然 64 位的虚拟内存地址空间已经让这种限制变得不太严重了)。

每个系统级线程都会有一个固定大小的栈(一般默认可能是2MB),这个栈主要用来保存函数递归调用时的参数和局部变量。固定了栈的大小导致了两个问题:一是对于很多只需要很小的栈空间的线程是一个巨大的浪费;二是对于少数需要巨大栈空间的线程又面临栈溢出的风险。针对这两个问题的解决方案是:要么降低固定的栈大小,提升空间的利用率;要么增大栈的大小以允许更深的函数递归调用,但这两者是无法兼得的。相反,一个Goroutine会以一个很小的栈启动(可能是2KB或4KB),当遇到深度递归导致当前栈空间不足时,Goroutine会根据需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。

Go的运行时还包含了其自己的调度器,这个调度器使用了一些技术手段,可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作原理和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度,只有在当前Goroutine发生阻塞时才会导致调度;同时发生在用户态,调度器会根据具体函数只保存必要的寄存器,切换的代价要比系统线程低得多。运行时有一个runtime.GOMAXPROCS变量,用于控制当前运行正常非阻塞Goroutine的系统线程数目。

goroutine 只是由官方实现的超级"线程池"。

每个实例2KB (在1.4新版本发布的运行时信息当中明确指出,从以前的1.2版本到1.3版本协程占用大小4kb到8kb,到现在的2kb左右,是一个性能上和的大跃进。)的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。

并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

单点Server的N种并发模型汇总

协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度,所以也不难理解golang中调度器的存在。协程的概念并不是与线程对应的,应该说和函数调用 call/return对应(也不难理解为什么会把golang中的goruntine当作一个以函数为单位的执行单元)。它们的区别在于协程允许一个函数有多个入口、出口(逻辑上的),并且在切换到另一个函数执行时,允许使用一个新的context(包括调用栈)。正是有了这个机制基础,再加上CPU支持了保护模式,操作系统就可以接着实现进程、线程了。

多路复用

Go 网络模型中另一个重要的机制是对 I/O 多路复用的封装。

在同步编程模式下,Go 真正的阻塞并未发生在操作系统调用的阻塞上,而是发生在用户态协程的阻塞上。借助不同操作系统下多路复用的封装以及非阻塞的 I/O 模式,当可用的 Socket 准备就绪,Go 就能保证之前陷入堵塞的协程可以运行,并最终被调度器调度。Go 调度器牢牢地锁定了协程的控制权,即便协程发生阻塞,调度器也能够快速切换到其他协程运行,在高并发网络 I/O 密集的环境下保证了程序的高性能。

0 人点赞