type
status
date
slug
summary
tags
category
icon
password
AI summary

Go语言基础之并发


1.串行、并发与并行的概念

1.1 串行 (Serial)

  • 定义:程序的任务或操作按照严格的时间顺序逐一执行,一个任务完成后才开始下一个任务。
  • 例子
    • 做完一件事情,再开始做下一件事情。
    • 一个线程逐条执行代码。

1.2 并发 (Concurrency)

  • 定义:多个任务在同一时间间隔内执行,但在某一时刻,每个处理器核心只执行一个任务。任务之间快速切换,表现为“同时”执行。
  • 例子
    • 操作系统中的多任务处理。
    • 在多个任务之间切换,给人一种同时执行的假象。

1.3 并行 (Parallelism)

  • 定义:多个任务真正地在同一时间执行。任务运行在不同的处理器或不同的核心上,是真正意义上的“同时”执行。
  • 例子
    • 两个线程分别运行在两个 CPU 核心上,各自执行独立任务。

串行、并发与并行的对比

特性
串行
并发
并行
执行顺序
一个接一个
快速切换任务
同时执行任务
时间消耗
最长时间
取决于任务切换效率
最短时间(硬件支持)
适用场景
简单逻辑或单任务执行
任务交互,I/O 密集型
计算密集型任务

进程、线程和协程的概念

1.4 进程 (Process)

  • 定义:进程是计算机中一个独立运行的程序实例,包含程序代码、数据和系统资源。
  • 特点
    • 进程之间是完全独立的。
    • 每个进程有自己独立的内存空间。
    • 进程间通信(IPC)成本较高。
    • 由操作系统调度和管理。
  • 例子
    • 一个浏览器是一个进程,每个打开的标签页也可能是独立的进程。

1.5 线程 (Thread)

  • 定义:线程是进程内部的执行单元,每个线程共享进程的内存和资源。
  • 特点
    • 一个进程可以有多个线程。
    • 线程之间可以共享内存,因此通信开销低。
    • 线程的创建和切换开销小,但可能导致竞争和死锁问题。
    • 由操作系统调度。
  • 例子
    • 一个网页浏览器(进程)中加载网页内容的线程与处理用户输入的线程。

1.6 协程 (Coroutine)

  • 定义:协程是比线程更轻量级的执行单元,由程序而非操作系统调度。
  • 特点
    • 协程通过协作切换,不需要操作系统的上下文切换。
    • 轻量级,适合高并发场景。
    • 一个线程可以运行多个协程。
    • 通常由语言的运行时(如 Go、Python 等)管理。
  • 例子
    • Go语言中的 goroutine。
    • Python 中的 asyncio

进程、线程和协程的对比

特性
进程
线程
协程
调度者
操作系统
操作系统
程序
开销
最大
较小
最小
通信成本
较低(共享内存)
极低(同一线程内切换)
并发能力
依赖操作系统
极高
应用场景
高隔离场景
I/O 密集型
高并发场景

2. Goroutine

2.1 什么是 Goroutine?

Goroutine 是 Go语言实现并发的基本单位。它比线程更轻量级,能够高效地运行成千上万个 goroutine。Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
特点:
  1. Goroutine 是由 Go语言的运行时调度管理的。
  1. 创建一个 goroutine 的开销非常小。
  1. 通过 go 关键字启动。
go关键字:
go语言中goroutine的使用方法很简单,只需要在函数或者方法前加go关键字
匿名函数
示例:
输出(可能顺序不同):
启动单个goroutine
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。通过sleep函数让主函数等待goroutine执行结束
输出(可能顺序不同):
优化
通过sleep来等待goroutine结束,这样的写法不优雅,另外等待的时间也不能确认,过长或过短。Go 语言中通过sync包为我们提供了一些常用的并发原语,介绍一下 sync 包中的WaitGroup。当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法。
下面的示例代码中我们在 main goroutine 中使用sync.WaitGroup来等待 hello goroutine 完成后再退出。
启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

2.2 Goroutine 的生命周期

  1. 创建:通过 go 关键字启动。
  1. 执行:由 Go 的调度器分配 CPU 时间片执行。
  1. 结束:goroutine 的任务完成后,自动退出。
注意: 主程序退出时,所有 goroutine 都会被强制终止。

3. Channel

3.1 什么是 Channel?

Channel 是 Go语言中 goroutine 之间进行通信的管道。可以通过 channel 在 goroutine 之间发送和接收数据。
特性:
  • Channel 是类型安全的。
  • 支持同步和阻塞操作。
  • 声明方式:chan 数据类型
举几个例子:

3.2 创建 Channel

使用 make 创建 channel。

3.3 发送和接收数据

示例:
输出:
注意:
  • 发送和接收操作会阻塞,直到另一端准备好。
  • 这种阻塞机制可以用来同步 goroutine。

3.4 带缓冲的 Channel

默认情况下,channel 是无缓冲的。可以通过指定缓冲区大小创建带缓冲的 channel。
示例:
输出:

3.5 关闭 Channel

通过 close 关闭 channel。被关闭的 channel 无法再发送数据,但可以继续接收数据(直到缓冲区为空)。
示例:
输出:

4. Select 语句

select 是 Go语言中的多路复用机制,可以同时监听多个 channel。
示例:
输出:

5. WaitGroup

sync.WaitGroup 用于等待一组 goroutine 执行完成。
示例:
输出:

6. 并发的最佳实践

  1. 避免共享数据竞争:通过 channel 或 sync 包同步 goroutine。
  1. 不要滥用 goroutine:每个 goroutine 都需要内存和调度资源。
  1. 清理未使用的 channel:确保及时关闭不再使用的 channel。
  1. 使用工具检测数据竞争:Go 提供了 race 参数检测竞争问题。

7. 并发安全的概念

在多线程或多 goroutine 环境中,多个操作可能同时访问共享资源。如果这些操作没有得到正确的同步,可能会导致数据竞争、死锁等问题,从而破坏程序的正确性。
并发安全指的是程序能够在多个线程或 goroutine 环境下,保证共享资源的正确访问。

8. 数据竞争

8.1 什么是数据竞争?

数据竞争是指多个 goroutine 同时访问同一块内存,其中至少一个操作是写入,且操作之间缺乏同步机制。
示例:
输出(可能不一致):

8.2 如何避免数据竞争?

通过同步原语(如互斥锁)或避免共享可变状态来防止数据竞争。

9. 互斥锁(Mutex)

9.1 什么是互斥锁?

互斥锁(Mutex)是一种同步机制,用于保护共享资源,确保同一时间只有一个 goroutine 能够访问该资源。
sync.Mutex提供了两个方法供我们使用。
方法名
功能
func (m *Mutex) Lock()
获取互斥锁
func (m *Mutex) Unlock()
释放互斥锁
示例:
输出(正确):

9.2 使用建议

  1. 保持锁粒度小:只保护必要的代码块,减少锁的持有时间。
  1. 避免死锁:注意锁的获取顺序,避免循环依赖。
  1. 使用 defer 解锁:确保锁能够正确释放。

10. 读写锁(RWMutex)

10.1 什么是读写锁?

sync.RWMutex 是一种允许多个 goroutine 同时读取共享资源,但写操作是独占的锁。

10.2 使用示例

输出:

11. 原子操作

11.1 什么是原子操作?

原子操作是不可分割的操作,能够保证在多 goroutine 环境下的安全性。
Go 提供了 sync/atomic 包来支持原子操作。

11.2 使用示例

输出:

小结

  • Go语言通过 goroutine 和 channel 提供了强大的并发支持。
  • sync.WaitGroupselect 是常用的并发工具。
  • 合理设计并发模型可以提高程序性能并减少复杂度。
  • 互斥锁(Mutex) 用于保护共享资源,防止数据竞争。
  • 读写锁(RWMutex) 适合读多写少的场景,提高并发性能。
  • 原子操作 提供高效的并发安全操作,适合简单计数场景。
并发是 Go语言的重要特性,熟练掌握并发工具和模式将显著提升开发效率和代码质量。
网络编程反射