串行、并行、并发

让我们想象一个人慢跑。当他晨跑时,假设他的鞋带解开了。现在这个人停止了跑步,系好鞋带,然后又开始跑步。这是并发的一个典型示例。这个人能够处理跑步和系鞋带,也就是说,这个人能够同时处理很多事情。

  • 并行指的是多个事情在 同一个时间点 上同时发生了。

  • 并发指的是多个事情在 同一时间段 内同时发生了。

Go 是一种并发语言,而不是一种并行语言。

进程(Process)(并行运算,分布式)

  • 定义:进程就是程序在操作系统中的一次执行过程,是系统进行资源调度和分配的基本单位。每个进程都有自己的一套独立的地址空间,这意味着进程间的内存是不共享的,每个进程都像是操作系统中的一个独立实体。比如,你打开一个浏览器,这就是一个进程;再打开一个文档编辑器,这是另一个进程。每个进程都有自己的空间,不会互相干扰。

  • 资源:进程拥有独立的资源,包括内存、文件描述符、环境变量等。

  • 调度:进程由操作系统进行调度,操作系统决定哪个进程在何时运行。

单道批处理系统内存中始终只保持一道作业,cpu 只能将内存中作业执行完毕,才能执行下一道作业,进程之间串行执行,A、B、C 三个进程按顺序执行

分时系统引申出了时间片的概念,进程按照调度算法分时间片在 CPU 上执行,A、B、C 三个进程按照时间片并发执行

线程(Thread)(并发执行)

在进程的基础上再细分出线程,线程比进程更轻量,线程之间的通信也更为便捷,任务的最小载体变成了线程。

  • 定义:线程是进程中的一个实体,是被系统独立调度和分派的基本单位,是程序执行的一个最小单位。线程自身不拥有系统资源,只拥有一点在运行中必不可少的资源(如执行栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。

  • 并行执行:在多核处理器上,同一个进程中的多个线程可以并行执行,即同时在不同的处理器上运行。

  • 共享资源:同一进程内的线程共享进程的资源,包括内存、文件句柄等,但每个线程有自己的程序计数器、寄存器和堆栈。

CPU 内核负责调度,为系统提供并发处理能力。

协程是在用户空间实现的, CPU 并不知道有 “用户态线程” 的存在,CPU 只知道它运行的是一个 “内核态线程”。

这里我们可以把 内核线程依然叫“线程(thread)”,用户线程叫“协程(co-routine)”。

线程越多,进程利用(或者)抢占的cpu资源就越高。

进程和线程的创建、切换和销毁都会消耗大量 CPU 资源。

每个线程约需 4MB 内存,大量线程会导致内存消耗过高。应用层无法直接控制内核调度,只能通过减少线程创建和切换来优化性能。这促生了协程的概念:用户级别的轻量线程。

协程(Coroutine)(并发执行)

  • 定义:协程是一种程序组件,它允许挂起和恢复执行。goroutine是由Go运行时管理的轻量级线程,与coroutine只能运行在一个线程上不同,goroutine可以运行在一个或多个线程上

  • 轻量级:goroutine比线程更加轻量级,因为它们的栈是动态的,并且可以在运行时调整大小,它们的堆栈大小只有几 kb,并且堆栈可以根据应用程序的需要进行扩展和收缩,而对于线程,则必须指定堆栈大小并固定堆栈大小。

  • 并发执行:在Go语言中,goroutine可以在单个操作系统线程中并发执行,Go运行时会负责调度这些goroutine,让它们在线程之间高效地共享和切换。

  • 共享资源:多个goroutine共享同一个线程的资源,但拥有自己的栈和寄存器。它们通过channel进行通信,这是一种同步机制,可以用来传递数据和同步goroutine。

Go 协程和线程的区别

Go 语言引入了 goroutine(协程),它是一个比线程更轻量级的并发执行单元。

  • 更小的内存占用

    • 每个线程的栈内存通常是固定的(通常为 1MB 或更多)。

    • goroutine 的栈内存默认仅占用约 2KB,可以根据需要自动增长和收缩。

  • 调度机制不同

    • 线程由操作系统调度,切换线程的开销较大,因为需要在用户态和内核态之间切换。

    • goroutine 是由 Go 运行时(runtime)调度的,属于用户态调度,切换开销更小。

    什么是runtime?

    Go语言程序执行的环境,它提供了一些底层服务,并发支持(goroutine)和协程调度垃圾回收内存管理反射,网络和文件I/O

  • 数量可达数十万

    • 创建一个线程的系统开销很大,不适合大规模并发。

    • goroutine 非常轻量,因此可以轻松创建成千上万个 goroutine,并发处理大量任务。

goroutine 的基本使用

goroutine 是 Go 中的轻量级线程,启动一个 goroutine 只需要使用 go 关键字,非常高效。每个 goroutine 都是独立的,多个 goroutine 可以并发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func hello() {
fmt.Println("Hello world goroutine")
}

func main() {
go hello()
fmt.Println("main function")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(100 * time.Millisecond) // 延迟 100 毫秒
}
}

func main() {
go printNumbers() // 启动一个新的 goroutine,并发执行 printNumbers 函数
fmt.Println("这是在主 goroutine 中")

time.Sleep(600 * time.Millisecond) // 等待 printNumbers 执行完
fmt.Println("主 goroutine 结束")
}

1
2
3
4
5
6
7
这是在主 goroutine 中
1
2
3
4
5
主 goroutine 结束

参考链接

操作系统篇一:进程与线程、并发并行与串行、同步与异步、阻塞与非阻塞当你被问到这些问题:你觉得的并发、并行、串行有什么区别 - 掘金

第三篇、Golang编程设计与通用之路 - 3、对于操作系统而言进程、线程以及Goroutine协程的区别 - 《Golang 修养之路》 - 书栈网 · BookStack

翻译来自 Goroutines

《Go语言轻松进阶:从入门、实战到内核揭秘》

GMP模型


本站由 GINA 使用 Stellar 1.29.1 主题创建。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

Copyright © 2024 GINA 保留所有权利。