简介
Go 是一种并发编程语言,它为编写并发和并行程序提供了内置支持。本教程将介绍 Go 同步的基本概念,包括原子操作、互斥锁和等待组的使用,以帮助你编写高效且线程安全的 Go 代码。
Go 是一种并发编程语言,它为编写并发和并行程序提供了内置支持。本教程将介绍 Go 同步的基本概念,包括原子操作、互斥锁和等待组的使用,以帮助你编写高效且线程安全的 Go 代码。
Go 是一种并发编程语言,这意味着它为编写并发和并行程序提供了内置支持。在 Go 中,并发是通过使用 goroutine 来实现的,goroutine 是轻量级的执行线程。然而,当多个 goroutine 访问共享资源时,可能会导致竞态条件和其他同步问题。本节将介绍 Go 同步的基本概念,包括原子操作、互斥锁和等待组的使用。
Go 提供了一组原子操作,允许你对原始数据类型执行低级别的、线程安全的操作。这些操作是使用 sync/atomic 包实现的,包括 atomic.AddInt32()、atomic.LoadInt32() 和 atomic.StoreInt32() 等函数。原子操作对于实现计数器、标志和其他共享变量很有用,无需显式锁定。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int32
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
atomic.AddInt32(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
在这个例子中,我们使用 atomic.AddInt32() 函数以线程安全的方式递增共享的 counter 变量。sync.WaitGroup 用于确保在打印计数器的最终值之前,所有 goroutine 都已完成。
互斥锁(“互斥”的缩写)是 Go 中常见的同步原语。它们用于保护共享资源不被多个 goroutine 同时访问。sync.Mutex 类型分别提供了 Lock() 和 Unlock() 方法来获取和释放互斥锁。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Mutex
var balance int
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
m.Lock()
defer m.Unlock()
balance += 100
}()
}
wg.Wait()
fmt.Println("Final balance:", balance)
}
在这个例子中,我们使用 sync.Mutex 来保护 balance 变量不被多个 goroutine 同时访问。每个 goroutine 在修改余额之前获取互斥锁,确保更新以线程安全的方式执行。
等待组是 Go 中的另一个同步原语,用于等待一组 goroutine 完成执行。sync.WaitGroup 类型提供了 Add()、Done() 和 Wait() 方法来管理 goroutine 组。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
fmt.Println("Goroutine 1 finished")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 2 finished")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 3 finished")
}()
wg.Wait()
fmt.Println("All goroutines have finished")
}
在这个例子中,我们创建了一个 sync.WaitGroup 并将其计数器加 3。然后我们启动三个 goroutine,每个 goroutine 在完成工作时调用 wg.Done()。最后,我们调用 wg.Wait() 来阻塞,直到所有 goroutine 都完成。
Go 提供了几种内置的同步机制,以帮助开发者管理对共享资源的并发访问。这些机制包括互斥锁、等待组和通道,每种机制都有其各自的用例和权衡之处。
互斥锁(“互斥”的缩写)用于保护共享资源不被多个 goroutine 同时访问。sync.Mutex 类型分别提供了 Lock() 和 Unlock() 方法来获取和释放互斥锁。当你需要确保一段关键代码一次仅由一个 goroutine 执行时,互斥锁很有用。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Mutex
var balance int
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
m.Lock()
defer m.Unlock()
balance += 100
}()
}
wg.Wait()
fmt.Println("Final balance:", balance)
}
等待组用于等待一组 goroutine 完成执行。sync.WaitGroup 类型提供了 Add()、Done() 和 Wait() 方法来管理 goroutine 组。当你需要确保所有 goroutine 都完成后再继续执行程序的其余部分时,等待组很有用。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
fmt.Println("Goroutine 1 finished")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 2 finished")
}()
go func() {
defer wg.Done()
fmt.Println("Goroutine 3 finished")
}()
wg.Wait()
fmt.Println("All goroutines have finished")
}
通道是 Go 中一种强大的同步机制,用于在 goroutine 之间进行通信。通道可用于在 goroutine 之间传递值,以及用于信号通知任务的完成。通道可以是带缓冲的或无缓冲的,它们提供了一种协调并发代码执行的方式。
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println("Received value:", value)
}
在这个例子中,我们创建了一个 int 类型的通道,然后启动一个 goroutine,该 goroutine 将值 42 发送到通道。主 goroutine 然后从通道接收值并打印它。
sync.Cond 类型提供了一种等待和信号通知共享条件变化的方式。当你需要基于特定条件协调多个 goroutine 的执行时,条件变量很有用。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Mutex
cond := sync.NewCond(&m)
var ready bool
go func() {
time.Sleep(2 * time.Second)
m.Lock()
ready = true
cond.Broadcast()
m.Unlock()
}()
m.Lock()
for!ready {
cond.Wait()
}
m.Unlock()
fmt.Println("Ready!")
}
在这个例子中,我们使用一个条件变量来等待共享的 ready 标志被设置为 true。设置该标志的 goroutine 然后广播该条件,允许等待的 goroutine 继续执行。
虽然 Go 提供的内置同步机制功能强大且用途广泛,但使用这些原语也可以实现几种常见的同步模式。在本节中,我们将探讨其中一些实用模式以及它们如何在实际应用中使用。
生产者 - 消费者模式是一种常见的并发模式,其中一个或多个生产者 goroutine 生成数据,一个或多个消费者 goroutine 处理该数据。此模式可以使用通道在生产者和消费者之间进行通信来实现。
package main
import (
"fmt"
"sync"
)
func main() {
const numProducers = 3
const numConsumers = 2
var wg sync.WaitGroup
wg.Add(numProducers + numConsumers)
ch := make(chan int, 10)
for i := 0; i < numProducers; i++ {
go func() {
defer wg.Done()
produceData(ch)
}()
}
for i := 0; i < numConsumers; i++ {
go func() {
defer wg.Done()
consumeData(ch)
}()
}
wg.Wait()
}
func produceData(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
}
func consumeData(ch <-chan int) {
for v := range ch {
fmt.Println("Consumed:", v)
}
}
在这个例子中,我们创建了一个通道来在生产者和消费者 goroutine 之间进行通信。生产者生成数据并将其发送到通道,而消费者从通道读取并处理数据。
当你有共享数据,且读取操作比写入操作更频繁时,读写锁模式很有用。sync.RWMutex 类型提供了 RLock()、RUnlock()、Lock() 和 Unlock() 方法来管理对共享数据的读写访问。
package main
import (
"fmt"
"sync"
)
func main() {
var rwm sync.RWMutex
var count int
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
rwm.RLock()
defer rwm.RUnlock()
fmt.Println("Count:", count)
}()
}
for i := 0; i < 10; i++ {
go func() {
rwm.Lock()
defer rwm.Unlock()
count++
}()
}
wg.Wait()
fmt.Println("Final count:", count)
}
在这个例子中,我们使用 sync.RWMutex 来保护 count 变量。读取器 goroutine 获取读锁以访问变量,而写入器 goroutine 获取写锁以修改变量。
信号量是一种同步原语,可用于限制并发操作的数量。在 Go 中,可以使用 chan struct{} 类型来实现信号量。
package main
import (
"fmt"
"sync"
)
func main() {
const maxConcurrent = 5
var wg sync.WaitGroup
wg.Add(10)
sem := make(chan struct{}, maxConcurrent)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
fmt.Println("Executing task...")
}()
}
wg.Wait()
}
在这个例子中,我们创建了一个大小为 maxConcurrent 的带缓冲通道作为信号量。每个 goroutine 尝试向通道发送一个值,如果通道已满则会阻塞。当一个 goroutine 完成其任务时,它从通道中移除一个值,允许另一个 goroutine 继续。
屏障是一种同步原语,它允许一组 goroutine 等待,直到它们全部到达执行中的某个特定点。在 Go 中,可以使用 sync.WaitGroup 类型来实现屏障。
package main
import (
"fmt"
"sync"
)
func main() {
const numGoroutines = 5
var wg sync.WaitGroup
wg.Add(numGoroutines)
var barrier sync.WaitGroup
barrier.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d is running...\n", id)
barrier.Done()
barrier.Wait()
fmt.Printf("Goroutine %d has passed the barrier\n", id)
}(i)
}
barrier.Wait()
fmt.Println("All goroutines have passed the barrier")
wg.Wait()
}
在这个例子中,我们使用一个名为 barrier 的 sync.WaitGroup 来实现屏障。每个 goroutine 到达屏障时调用 barrier.Done(),然后调用 barrier.Wait() 等待所有其他 goroutine 到达屏障后再继续。
sync.Pool 类型是一种同步原语,可用于管理可重用对象的池。在需要创建和销毁许多相似对象的情况下,这对于提高性能可能很有用。
package main
import (
"fmt"
"sync"
)
func main() {
var pool = &sync.Pool{
New: func() interface{} {
return &myStruct{
data: make([]byte, 1024),
}
},
}
var wg sync.WaitGroup
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
defer wg.Done()
obj := pool.Get().(*myStruct)
defer pool.Put(obj)
// 使用对象
_ = obj.data
}()
}
wg.Wait()
}
type myStruct struct {
data []byte
}
在这个例子中,我们创建了一个 sync.Pool,它管理 myStruct 对象的池。当一个 goroutine 需要使用这些对象之一时,它调用 pool.Get() 从池中检索一个对象,使用完后调用 pool.Put() 将对象返回给池。
在本教程中,你已经学习了 Go 同步的基础知识,包括原子操作、互斥锁和等待组的使用。这些同步原语对于在 Go 中编写并发和并行程序至关重要,因为它们可帮助你避免竞态条件和其他同步问题。通过理解这些概念,你将更有能力编写高效且线程安全的 Go 代码,从而利用该语言内置的并发支持。