Go 内存同步
在并发编程中,多个goroutine可能会同时访问和修改共享数据。如果没有适当的同步机制,可能会导致数据竞争(data race)和不可预测的行为。Go语言提供了多种内存同步机制,以确保并发程序中的数据一致性。本文将详细介绍Go中的内存同步概念、同步原语及其实际应用。
什么是内存同步?
内存同步是指在并发程序中,通过某种机制确保多个goroutine对共享数据的访问是有序的,从而避免数据竞争。数据竞争发生在两个或多个goroutine同时访问同一块内存区域,且至少有一个访问是写操作时。
Go语言的内存模型定义了goroutine之间的操作顺序,以及如何通过同步原语(如sync.Mutex
、sync.WaitGroup
、sync.Once
等)来确保内存同步。
同步原语
sync.Mutex
sync.Mutex
是Go语言中最常用的同步原语之一。它提供了互斥锁机制,确保同一时间只有一个goroutine可以访问共享资源。
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
输出:
Counter: 1000
在这个例子中,sync.Mutex
确保了counter
的递增操作是线程安全的。每个goroutine在修改counter
之前都会先获取锁,修改完成后释放锁。
sync.WaitGroup
sync.WaitGroup
用于等待一组goroutine完成。它通常用于主goroutine等待其他goroutine完成后再继续执行。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
输出:
Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers done
在这个例子中,sync.WaitGroup
用于等待所有worker
goroutine完成后再继续执行主goroutine。
sync.Once
sync.Once
确保某个操作只执行一次,即使在多个goroutine中调用也是如此。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func setup() {
fmt.Println("Initialization complete")
}
func doSomething() {
once.Do(setup)
fmt.Println("Doing something")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doSomething()
}()
}
wg.Wait()
}
输出:
Initialization complete
Doing something
Doing something
Doing something
Doing something
Doing something
在这个例子中,sync.Once
确保了setup
函数只执行一次,即使有多个goroutine调用doSomething
。
实际应用场景
并发缓存
在实际应用中,内存同步常用于实现并发安全的缓存。例如,可以使用sync.Mutex
来保护缓存的读写操作。
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.Mutex
items map[string]string
}
func NewCache() *Cache {
return &Cache{
items: make(map[string]string),
}
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
value, ok := c.items[key]
return value, ok
}
func main() {
cache := NewCache()
cache.Set("foo", "bar")
value, ok := cache.Get("foo")
if ok {
fmt.Println("Value:", value)
} else {
fmt.Println("Key not found")
}
}
输出:
Value: bar
在这个例子中,Cache
结构体使用sync.Mutex
来保护items
映射的读写操作,确保并发安全。
总结
Go语言提供了多种同步原语来帮助开发者实现内存同步,避免数据竞争。通过合理使用sync.Mutex
、sync.WaitGroup
、sync.Once
等同步原语,可以确保并发程序中的数据一致性。
附加资源
练习
- 修改上面的并发缓存示例,使其支持并发删除操作。
- 使用
sync.WaitGroup
实现一个并发任务调度器,确保所有任务完成后才退出主程序。 - 使用
sync.Once
实现一个单例模式,确保某个对象只被初始化一次。
通过这些练习,你将更深入地理解Go语言中的内存同步机制。