1. Goroutines 概述
Goroutines 是 Go 语言并发编程中的重要概念,是轻量级的线程。与传统并发编程模型的线程相比,Goroutines 的优势在于首先在创建和销毁时代价非常小,可以轻松地启动千万级别的 Goroutines,而线程则无法做到。
一个 Goroutine 可以理解成一个并发执行的函数。通过 go 关键字,可以很方便地启动一个 Goroutine。Go 会自动为我们管理 Goroutines 的生命周期,使得我们可以轻松实现并发编程。
func printHello() {
fmt.Println("Hello")
}
func main() {
// 启动一个 Goroutine
go printHello()
fmt.Println("World")
}
在上面的例子中,我们启动了一个在后台并发执行的 Goroutine,并在主函数中打印了 "World"。因为启动 Goroutine 的操作是非阻塞的,所以 "World" 会先被执行。如果没有使用 Goroutines,打印 "Hello" 将会在打印 "World" 后才执行。
2. Goroutines 同步
2.1 WaitGroup 同步机制
当我们启动多个 Goroutines 时,我们往往需要让这些并发执行的 Goroutines 之间同步。例如,我们需要在所有 Goroutines 执行完之后,再进行下一步操作。这时我们可以使用 WaitGroup 同步机制。
WaitGroup 是一个计数器,它用于记录 Goroutines 的数量。每个 Goroutine 执行完成之后,计数器减 1,当计数器的值为 0 时,表示所有 Goroutines 都已经执行完成。主线程可以通过调用 Wait 方法来等待所有 Goroutines 完成后再继续执行。
func worker(id int, wg *sync.WaitGroup) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
// 计数器减 1
wg.Done()
}
func main() {
var wg sync.WaitGroup
// 启动 3 个 Goroutines
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有 Goroutines 完成
wg.Wait()
fmt.Println("All workers Done")
}
在上面的代码中,我们定义了一个 worker 函数,它接受一个整数 id 和一个 WaitGroup 指针作为参数。在函数中,我们模拟了一个耗时的任务,并在执行完成后打印了一些信息。在主函数中,我们启动了 3 个 Goroutines,并通过 Add 方法将计数器加 1。在每个 Goroutine 中执行完成后,计数器会减 1,并在主函数中调用 Wait 方法等待所有 Goroutines 执行完成。最后输出 "All workers Done"。
2.2 Channel 同步机制
在并发编程中,Channel 是一个非常常用的同步机制。它可以让 Goroutines 可以安全地进行相互通信。
Channel 是 Goroutines 之间数据传输的管道。一个 Goroutine 可以向 Channel 中发送数据,另一个 Goroutine 可以从 Channel 中接收数据。
func producer(ch chan int) {
for i := 1; i <= 10; i++ {
ch <- i
}
// 关闭 Channel
close(ch)
}
func consumer(ch chan int) {
for {
// 从 Channel 中读取数据
num, ok := <- ch
if !ok {
break
}
fmt.Println(num)
}
}
func main() {
// 创建一个有缓冲的 Channel
ch := make(chan int, 5)
// 启动生产者 Goroutine
go producer(ch)
// 启动消费者 Goroutine
go consumer(ch)
// 阻塞主函数
select {}
}
在上面的代码中,我们定义了一个生产者 Goroutine 和一个消费者 Goroutine,两个 Goroutine 之间通过 Channel 进行通信。生产者 Goroutine 会向 Channel 中不断发送数据,消费者 Goroutine 从 Channel 中读取数据,并打印到控制台上。当生产者发送完所有数据时,通过关闭 Channel 来告诉消费者 Goroutine 已经完成了任务。
3. Goroutines 互斥
3.1 Mutex 互斥机制
当多个 Goroutines 需要访问同一个共享资源时,我们需要对这个资源进行互斥保护,防止多个 Goroutines 同时对其进行写操作,从而导致数据出现错误。Go 语言中提供了 Mutex 互斥锁来解决这个问题。
var (
count int
lock sync.Mutex
)
func worker(id int, wg *sync.WaitGroup) {
fmt.Printf("Worker %d starting\n", id)
for i := 0; i < 10000; i++ {
// 对共享变量 count 进行互斥保护
lock.Lock()
count++
lock.Unlock()
}
fmt.Printf("Worker %d done\n", id)
// 计数器减 1
wg.Done()
}
func main() {
var wg sync.WaitGroup
// 启动 3 个 Goroutines
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有 Goroutines 完成
wg.Wait()
fmt.Printf("count: %d\n", count)
}
在上面的代码中,我们定义了一个共享变量 count 和一个 Mutex 互斥锁 lock。多个 Goroutines 在执行时,虽然会对 count 进行写操作,但是通过对 lock 进行互斥保护,保证了同一时间只有一个 Goroutine 可以对 count 进行读写操作。
3.2 RWMutex 读写锁机制
虽然 Mutex 互斥锁可以保证线程安全,但是却无法充分利用并发的优势。因为每次对共享资源进行读操作时都需要进行互斥保护,导致读操作无法并发执行。RWMutex 读写锁解决了这个问题。
RWMutex 是 sync 包提供的一种读写锁,读写锁区分读操作和写操作。多个 Goroutines 可以同时对同一个共享资源进行读取,但是一旦有一个 Goroutine 对该共享资源进行写操作,就必须阻塞其他所有 Goroutines 进行读写操作。这样就能够充分利用并发的优势,提高程序的性能。
var (
count int
rwlock sync.RWMutex
)
func read(id int, wg *sync.WaitGroup) {
rwlock.RLock()
defer rwlock.RUnlock()
num := count
fmt.Printf("Goroutine %d reads value %d\n", id, num)
wg.Done()
}
func write(id int, wg *sync.WaitGroup) {
rwlock.Lock()
defer rwlock.Unlock()
count++
fmt.Printf("Goroutine %d writes value %d\n", id, count)
wg.Done()
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
// 读 Goroutine
wg.Add(1)
go read(i, &wg)
}
for i := 1; i <= 2; i++ {
// 写 Goroutine
wg.Add(1)
go write(i, &wg)
}
// 等待所有 Goroutines 完成
wg.Wait()
}
在上面的代码中,我们定义了两个 Goroutines,一个是 read Goroutine,一个是 write Goroutine。read Goroutine 用于对共享变量进行读取操作,write Goroutine 用于对共享变量进行写入操作。多个 read Goroutines 可以同时对共享变量进行读取,但是一旦有一个 write Goroutine 开始写入操作,所有的 read Goroutines 都需要进行阻塞,直到该 write Goroutine 执行完操作并释放锁。
4. 总结
Goroutines 是 Go 语言并发编程的重要概念之一,通过 Goroutines,可以轻松实现并发编程。在 Goroutines 的并发执行中,需要使用同步和互斥机制来保证数据的正确性和程序的稳定性,并提高程序的性能。WaitGroup 同步机制和 Channel 同步机制可以很好地解决 Goroutines 同步的问题,而 Mutex 互斥锁和 RWMutex 读写锁则可以很好地解决 Goroutines 互斥的问题。