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,直到它能获得锁。
WaitGroup: sync.WaitGroup 用于等待一组 Goroutine 完成。常用于主程序等待多个 Goroutine 完成任务。
- Add(n) :设置计数器的值,表示有 n 个任务。
- Done() :表示一个任务完成,计数器减 1。
- Wait() :阻塞直到计数器的值为 0。
Go 依赖管理
Go 使用 go.mod 和 go.sum 文件来管理依赖,类似于 Python 的 requirements.txt 和 JavaScript 的 package.json。Go 通过 Go Modules 来实现模块化的依赖管理。
-
go.mod 文件:记录项目的模块依赖,包括 Go 版本和依赖库。
go.mod会在go命令执行时自动生成或更新。
-
Go 依赖管理命令:
go get <package>:下载并安装依赖包。go mod tidy:整理go.mod和go.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) // 原子增加