0.1、索引
https://blog.waterflow.link/articles/1662974432717
1、进程
一个进程包含可以由任何进程分配的公共资源。这些资源包括但不限于内存地址空间、文件句柄、设备和线程。
一个进程会包含下面一些属性:
- Process ID:进程ID
- Process State:进程状态
- Process Priority:进程优先级
- Program Counter:程序计数器
- General purpose register:通用寄存器
- List of open files:打开的文件列表
- List of open devices:打开的设备列表
- Protection information:保护信息
- List of the child process:子进程列表
- Pending alarms:待定警告
- Signals and signal handlers:信号和信号处理程序
- Accounting information:记账信息
2、线程
线程是轻量级的进程,一个线程将在进程内的所有线程之间共享进程的资源,如代码、数据、全局变量、文件和内存地址空间。但是栈和寄存器不会共享,每个线程都有自己的栈和寄存器
线程的优点:
- 提高系统的吞吐量
- 提高响应能力
- 由于属性更少,上下文切换更快
- 多核CPU的有效利用
- 资源共享(代码、数据、地址空间、文件、全局变量)
3、用户级线程
用户级线程也称为绿色线程,如:C 中的coroutine、Go 中的 goroutine 和 Ruby 中的 Fiber
该进程维护一个内存地址空间,处理文件,以及正在运行的应用程序的设备和线程。操作系统调度程序决定哪些线程将在任何给定的 CPU 上接收时间
因此,与耗时和资源密集型的进程创建相比,在一个进程中创建多个用户线程(goroutine)效率更高。
4、goroutine
在Go中用户级线程被称作Goroutine,在创建goroutine时需要做到:
- 易于创建
- 轻量级
- 并发执行
- 可扩展
- 无限堆栈(最大堆栈大小在 64 位上为 1 GB,在 32 位上为 250 MB。)
- 处理阻塞调用
- 高效 (work stealing)
其中阻塞调用可能是下面一些原因:
- 在channel中收发数据
- 网络IO调用
- 阻塞的系统调用
- 计时器
- 互斥操作(Mutex)
为什么go需要调度goroutine?
Go 使用称为 goroutine 的用户级线程,它比内核级线程更轻且更便宜。 例如,创建一个初始 goroutine 将占用 2KB 的堆栈大小,而内核级线程将占用 8KB 的堆栈大小。 还有,goroutine 比内核线程有更快的创建、销毁和上下文切换,所以 go 调度器 需要退出来调度 goroutine。OS 不能调度用户级线程,OS 只知道内核级线程。 Go 调度器 将 goroutine 多路复用到内核级线程,这些线程将在不同的 CPU 内核上运行
什么时候会调度goroutine?
如果有任何操作应该或将会影响 goroutine 的执行,比如 goroutine 的启动、等待执行和阻塞调用等……
go调度 如何将 goroutine 多路复用到内核线程中?
1、1:1调度(1个线程对应一个goroutine)
- 并行执行(每个线程可以在不同的内核上运行)
- 可以工作但是代价太高
- 内存至少?32k(用户堆栈和内核堆栈的内存)
- 性能问题(系统调用)
- 没有无限堆栈
2、N:1调度(在单个内核线程上多路复用所有 goroutine)
- 没有并行性(即使有更多 CPU 内核可用,也只能使用单个 CPU 内核)
我们看下下面的例子,只为go分配了1个processer去处理2个goroutine:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
// 分配 1 个逻辑处理器供调度程序使用
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Starting Goroutines")
// 开一个go协程打印字母
go func() {
defer wg.Done()
time.Sleep(time.Second)
// 打印3次字母
for count := 0; count < 3; count++ {
for ch := 'a'; ch < 'a'+26; ch++ {
fmt.Printf("%c ", ch)
}
fmt.Println()
}
}()
// 开一个go协程打印数字
go func() {
defer wg.Done()
// 打印3次数字
for count := 0; count < 3; count++ {
for n := 1; n <= 26; n++ {
fmt.Printf("%d ", n)
}
fmt.Println()
}
}()
// 等待返回
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}
看下结果:
go run main.go
Starting Goroutines
Waiting To Finish
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program
可以看到这俩个goroutine是串行执行的,要么先完成第一个goroutine,要么先完成第二个goroutine,并不是并发执行的。
那如何去实现并发执行呢?
我们同样设置runtime.GOMAXPROCS为1,但是在goroutine中我们在不同的时机加入阻塞goroutine的时间函数time.Sleep,我们看下会有什么不同的结果。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
// 分配 1 个逻辑处理器供调度程序使用
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Starting Goroutines")
// 开一个go协程打印字母
go func() {
defer wg.Done()
time.Sleep(time.Secon