Go 语言并发
2026/7/19 6:05:04 网站建设 项目流程

在学习 Go 语言之前,我对并发的理解一直停留在一个比较浅的层面:

并发 = 同时运行多个任务。

比如:

  • 开一个线程下载文件;
  • 开多个线程处理请求;
  • 多个任务一起执行,提高效率。

但真正学习 Go 的并发模型后,我发现 Go 对并发的理解并不是简单地“让多个任务同时跑”。

Go 更关注的是:

如何让多个执行单元安全、高效地协作。

这也是 Go 语言设计哲学中非常重要的一部分:

不要通过共享内存来通信,而应该通过通信来共享内存。

这句话也成为了 Go 并发设计的核心。


一、为什么需要并发?

我们先思考一个问题:

假设一个程序需要完成三个任务:

任务A:读取文件 任务B:处理数据 任务C:发送网络请求

如果按照传统的顺序执行:

A → B → C

那么:

  • A 等待磁盘读取时,CPU 可能空闲;
  • C 等待网络响应时,程序也无法做其他事情。

大量时间浪费在等待上。

如果能够:

A \ B \ C

三个任务同时推进,那么程序整体效率就会提升。

这就是并发存在的意义:

充分利用计算资源,让程序在等待的时候继续做其他事情。


二、进程、线程、协程:Go 处在哪一层?

在理解 Go 并发之前,需要先区分几个概念。

1. 进程(Process)

进程是操作系统进行资源分配的基本单位。

例如:

打开浏览器:

Chrome进程

打开微信:

微信进程

每个进程都有:

  • 独立内存空间;
  • 文件资源;
  • 系统资源。

进程之间相互隔离。

优点:

  • 稳定;
  • 一个进程崩溃不会影响其他进程。

缺点:

  • 创建成本高;
  • 通信复杂。

2. 线程(Thread)

线程是进程内部的执行单位。

例如:

一个浏览器进程:

Chrome进程 ├── UI线程 ├── 网络线程 └── 渲染线程

多个线程共享进程资源。

优点:

  • 创建成本低;
  • 通信方便。

缺点:

共享数据容易产生问题。

例如:

两个线程同时修改一个变量:

count++

可能出现:

线程A读取 count = 0 线程B读取 count = 0 线程A +1 线程B +1 最终: count = 1

明明执行两次,结果却是 1。

这就是经典的:

线程安全问题。


3. 协程(Goroutine)

Go 没有直接让开发者操作线程。

而是提供:

Goroutine

它是一种轻量级执行单位。

例如:

普通线程:

线程1 线程2 线程3

可能需要 MB 级别内存。

而 Goroutine:

goroutine1 goroutine2 goroutine3 ... goroutine10000

可以轻松创建大量数量。

创建一个 Goroutine:

go task()

非常简单。

例如:

package main import ( "fmt" "time" ) func hello() { fmt.Println("hello goroutine") } func main() { go hello() time.Sleep(time.Second) }

这里:

go hello()

表示:

启动一个新的 Goroutine 执行 hello。


三、Goroutine 和线程有什么区别?

很多初学者会认为:

Goroutine = Go里面的线程

其实不是。

它们关系:

操作系统 ↓ 线程 Thread ↓ Go Runtime ↓ Goroutine

一个程序:

Go程序 ├── 线程1 │ ├── goroutine A │ ├── goroutine B │ ├── 线程2 ├── goroutine C └── goroutine D

Go Runtime 会负责:

  • 创建 Goroutine;
  • 调度 Goroutine;
  • 分配线程。

开发者不需要关心线程管理。

这也是 Go 简化并发开发的重要原因。


四、Go 的并发调度模型 GMP

Go 的并发调度依靠 GMP 模型。

三个核心:

G:Goroutine

表示用户创建的任务。

例如:

go func(){ fmt.Println("hello") }()

这个函数就是一个 G。


M:Machine

表示操作系统线程。

真正执行代码的是线程。


P:Processor

表示调度器资源。

负责:

  • 管理 Goroutine;
  • 分配给线程执行。

整体关系:

P G G G G G ↓ M 操作系统线程

简单理解:

G 是任务,M 是执行者,P 是调度管理员。


五、并发中的核心问题:数据共享

并发最大的难点不是启动多个任务。

而是:

多个任务同时修改数据怎么办?

例如:

count := 0 go func(){ count++ }() go func(){ count++ }()

两个 Goroutine 同时修改:

可能产生:

结果: 1 而不是: 2

怎么办?

Go 提供两种思想:


六、方式一:共享内存 + 锁

传统方式:

多个线程共享变量。

Go 提供:

sync.Mutex

例如:

var mutex sync.Mutex mutex.Lock() count++ mutex.Unlock()

含义:

修改数据之前:

加锁

其他 Goroutine:

等待

修改完成:

释放锁

这样保证安全。


七、方式二:Channel(Go推荐方式)

Go 更推荐:

不共享数据,而通过通信传递数据。

例如:

创建 channel:

ch := make(chan int)

发送数据:

ch <- 10

接收数据:

num := <-ch

例如:

func worker(ch chan int){ ch <- 100 } func main(){ ch := make(chan int) go worker(ch) result := <-ch fmt.Println(result) }

流程:

worker goroutine | | ↓ channel | | ↓ main goroutine

数据通过 channel 流动。


八、为什么 Go 推崇 Channel?

因为共享内存:

多个地方修改同一个变量

容易出现:

  • 加锁;
  • 死锁;
  • 数据竞争。

而 Channel:

生产者 ↓ Channel ↓ 消费者

更加符合现实世界。

类似:

工厂生产:

生产线 → 仓库 → 消费者

每个人负责自己的事情。


九、并发不是越多越好

学习并发后,一个容易犯的错误:

创建越多 Goroutine,程序越快。

实际上不是。

例如:

创建:

100万个goroutine

可能导致:

  • 调度压力增加;
  • 内存增加;
  • 上下文切换增加。

并发设计需要考虑:

  • CPU数量;
  • 任务类型;
  • 数据竞争。

合理的并发:

不是数量多。

而是:

让任务之间合理协作。


十、Go 并发带来的思考

学习 Go 并发之后,我最大的感受是:

以前学习多线程:

关注的是:

如何让多个线程同时运行?

而 Go:

关注的是:

多个任务之间如何合作完成目标。

这是一种编程思想上的变化。

并发并不是简单地把任务拆开。

真正困难的是:

  • 谁负责什么?
  • 数据如何流动?
  • 如何避免互相干扰?

Go 通过 Goroutine 降低了创建并发任务的成本,通过 Channel 提供了一种更安全的数据交换方式。

最终实现:

用简单的代码表达复杂的并发逻辑。

这也是 Go 语言能够在服务器开发、云计算、微服务领域广泛应用的重要原因。


总结

本文学习了 Go 并发中的几个核心概念:

  • 进程、线程、协程的区别;
  • Goroutine 的特点;
  • GMP 调度模型;
  • 并发中的数据竞争问题;
  • Mutex 和 Channel 的区别;
  • Go 的并发设计思想。

如果说传统并发是在解决:

“如何让多个任务一起运行?”

那么 Go 给出的答案是:

“如何让多个任务更优雅地协作。”

这也是 Go 语言并发设计最吸引人的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询