【青训营笔记】Goroutine 协程

54 阅读3分钟

Goroutine 和 Go 并发

go 就是为并发而生的

Goroutine (协程)是 Go 中轻量级的线程,是为了并发而设计的,能够高效地调度执行。

线程 和 协程

  • 协程:在用户态,轻量级线程 栈 是kb 级别
  • 线程:内核态,线程跑多个协程,栈MB级别

Go 可以轻松地创建成千上万个 Goroutine,几乎没有性能开销。

线程和协程的区别

  • 线程(Thread) :线程是操作系统级别的执行单位,每个线程拥有独立的栈空间,通常会消耗较大的系统资源。操作系统需要管理和调度线程,因此线程的创建和上下文切换相对较为昂贵。
  • 协程(Goroutine) :协程是由程序运行时(Go runtime)管理的执行单位,在用户态进行调度,内存消耗小。多个协程可以共享同一个线程,由 Go 的调度器管理,使得协程的创建和切换开销比线程要小得多。

协程通讯

Channel(通道)

Go 中的协程是通过 Channel 进行通信的。Channel 是 Go 中的同步原语,允许不同 Goroutine 之间进行数据交换。它提供了一种类型安全、简洁的通信方式。

Channel 的创建

ch := make(chan int)           // 无缓冲通道
ch := make(chan int, 10)       // 有缓冲通道,缓冲区大小为 10

Channel 类型:Channel 分为两类:

  • 无缓冲通道(同步通道) :每次发送数据时,必须有接收方准备好接收数据,否则会阻塞。这是最常见的用于同步的方式。
  • 有缓冲通道:可以缓存一定数量的数据,发送数据不会阻塞,直到缓冲区满或接收方取走数据。

并发安全和同步

并发编程中,多个 Goroutine 可能会同时访问共享数据,导致数据竞争,因此需要通过同步机制来保证数据的安全。

和 C ++ 一样使用锁来管理

Lock(锁) : Go 提供了 sync.Mutex 来保证对共享资源的互斥访问,防止并发读写冲突。

var mu sync.Mutex
mu.Lock()
// 临界区代码
mu.Unlock()

Lock 会阻塞当前 Goroutine,直到它能获得锁。

WaitGroupsync.WaitGroup 用于等待一组 Goroutine 完成。常用于主程序等待多个 Goroutine 完成任务。

  • Add(n) :设置计数器的值,表示有 n 个任务。
  • Done() :表示一个任务完成,计数器减 1。
  • Wait() :阻塞直到计数器的值为 0。

Go 依赖管理

Go 使用 go.modgo.sum 文件来管理依赖,类似于 Python 的 requirements.txt 和 JavaScript 的 package.json。Go 通过 Go Modules 来实现模块化的依赖管理。

  • go.mod 文件:记录项目的模块依赖,包括 Go 版本和依赖库。

    • go.mod 会在 go 命令执行时自动生成或更新。
  • Go 依赖管理命令

    • go get <package>:下载并安装依赖包。
    • go mod tidy:整理 go.modgo.sum 文件,移除不再使用的依赖。
  • Go Vendoring(依赖包管理)go mod vendor 会将项目的所有依赖包复制到本地的 vendor 目录中。这样可以保证项目在没有外部网络的情况下也能编译和运行。

Go 性能测试

Go 提供了内置的性能测试工具,使用 go test 命令进行性能基准测试:

go test -bench .  # 测试当前包的所有基准测试
go test -benchmem  # 测试并打印内存分配信息

基准测试文件通常命名为 xxx_test.go,并包含如下结构:

func BenchmarkExample(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // 待测试的代码
    }
}

Atomic(原子操作)

Go 提供了 sync/atomic包,允许开发者在并发环境下执行原子操作。原子操作可以确保对共享变量的操作不会被中断,避免了使用锁的开销。

一般加锁对程序执行 逻辑判断加锁 而 对变量的操作推荐使用原子操作

import "sync/atomic"var counter int32
atomic.AddInt32(&counter, 1)  // 原子增加