跳到主要内容

Go 内存同步

在并发编程中,多个goroutine可能会同时访问和修改共享数据。如果没有适当的同步机制,可能会导致数据竞争(data race)和不可预测的行为。Go语言提供了多种内存同步机制,以确保并发程序中的数据一致性。本文将详细介绍Go中的内存同步概念、同步原语及其实际应用。

什么是内存同步?

内存同步是指在并发程序中,通过某种机制确保多个goroutine对共享数据的访问是有序的,从而避免数据竞争。数据竞争发生在两个或多个goroutine同时访问同一块内存区域,且至少有一个访问是写操作时。

Go语言的内存模型定义了goroutine之间的操作顺序,以及如何通过同步原语(如sync.Mutexsync.WaitGroupsync.Once等)来确保内存同步。

同步原语

sync.Mutex

sync.Mutex是Go语言中最常用的同步原语之一。它提供了互斥锁机制,确保同一时间只有一个goroutine可以访问共享资源。

go
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完成后再继续执行。

go
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中调用也是如此。

go
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来保护缓存的读写操作。

go
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.Mutexsync.WaitGroupsync.Once等同步原语,可以确保并发程序中的数据一致性。

附加资源

练习

  1. 修改上面的并发缓存示例,使其支持并发删除操作。
  2. 使用sync.WaitGroup实现一个并发任务调度器,确保所有任务完成后才退出主程序。
  3. 使用sync.Once实现一个单例模式,确保某个对象只被初始化一次。

通过这些练习,你将更深入地理解Go语言中的内存同步机制。