本文最后更新于 2025-11-13,文章内容可能已经过时。

Go语言作为高并发编程的优秀语言,提供了丰富的同步原语来管理并发。下面我将详细介绍Go语言中常用的同步机制,包括适用场景和代码示例。

1. 互斥锁(Mutex)

原理

  • sync.Mutex是最基本的互斥锁,保证同一时间只有一个Goroutine可以访问临界区
  • 有Lock()和Unlock()两个方法
  • 互斥锁不支持重入(同一个Goroutine不能多次获取同一个锁)

适用场景

  • 读写操作频率相近的场景
  • 需要保护共享资源不被并发修改的场景
  • 读写不确定的场景(即读写次数没有明显区别)

代码示例

package main

import (
	"fmt"
	"sync"
	"time"
)

var counter int = 0
var lock sync.Mutex

func main() {
	var wg sync.WaitGroup
	
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 10; j++ {
				lock.Lock()
				counter++
				fmt.Printf("Goroutine %d sees counter: %d\n", id, counter)
				lock.Unlock()
				time.Sleep(time.Millisecond * 10)
			}
		}(i)
	}
	
	wg.Wait()
	fmt.Println("Final counter:", counter)
}

注意事项

  • 避免在锁内进行长时间操作(如I/O),否则会阻塞其他Goroutine
  • 使用defer lock.Unlock()确保锁能正确释放
  • 不要重复加锁,否则会导致死锁

2. 读写锁(RWMutex)

原理

  • sync.RWMutex是读写互斥锁
  • 允许多个Goroutine同时读取共享资源
  • 但写操作是互斥的,即写操作会阻塞其他读和写操作

适用场景

  • 读多写少的场景(如缓存系统、配置管理)
  • 需要高并发读取,但写操作相对较少的场景
  • 读操作频繁,写操作较少的场景

代码示例

package main

import (
	"fmt"
	"sync"
	"time"
)

var data int = 0
var rwMutex sync.RWMutex

func readData(id int) {
	for i := 0; i < 5; i++ {
		rwMutex.RLock()
		fmt.Printf("Reader %d: data is %d\n", id, data)
		rwMutex.RUnlock()
		time.Sleep(time.Millisecond * 50)
	}
}

func writeData(id int) {
	for i := 0; i < 3; i++ {
		rwMutex.Lock()
		data++
		fmt.Printf("Writer %d: updated data to %d\n", id, data)
		rwMutex.Unlock()
		time.Sleep(time.Millisecond * 100)
	}
}

func main() {
	var wg sync.WaitGroup
	
	// 启动多个读goroutine
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			readData(id)
		}(i)
	}
	
	// 启动多个写goroutine
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			writeData(id)
		}(i)
	}
	
	wg.Wait()
}

注意事项

  • 读锁和写锁是互斥的,写操作会阻塞所有读操作
  • 读锁可以重入(同一个Goroutine可以多次获取读锁)
  • 不要长时间持有写锁,以免阻塞所有读操作

3. 条件变量(Cond)

原理

  • sync.Cond用于在特定条件满足时唤醒等待的Goroutine
  • 通常与互斥锁一起使用
  • 有Wait()、Signal()和Broadcast() 方法

适用场景

  • 生产者-消费者模型
  • 等待特定条件满足的场景
  • 需要协调多个Goroutine的场景

代码示例

package main

import (
	"fmt"
	"sync"
	"time"
)

var items []int
var maxItems = 5
var mu sync.Mutex
var cond = sync.NewCond(&mu)

func producer() {
	for i := 1; i <= 10; i++ {
		mu.Lock()
		for len(items) >= maxItems {
			cond.Wait()
		}
		items = append(items, i)
		fmt.Printf("Produced: %d\n", i)
		mu.Unlock()
		cond.Signal()
		time.Sleep(time.Millisecond * 100)
	}
}

func consumer() {
	for {
		mu.Lock()
		for len(items) == 0 {
			cond.Wait()
		}
		item := items[0]
		items = items[1:]
		fmt.Printf("Consumed: %d\n", item)
		mu.Unlock()
		cond.Signal()
		time.Sleep(time.Millisecond * 200)
	}
}

func main() {
	go producer()
	go consumer()
	
	// 让程序运行一段时间
	time.Sleep(time.Second * 5)
}

注意事项

  • 条件变量必须与互斥锁一起使用
  • 在调用Wait()之前必须持有锁
  • 使用Signal()唤醒一个等待的Goroutine,使用Broadcast()唤醒所有等待的Goroutine

4. 原子操作(Atomic)

原理

  • sync/atomic包提供了对基本数据类型的原子操作
  • 保证对共享变量的读写操作是原子的
  • 常用操作:AddInt32、LoadInt32、StoreInt32、SwapInt32、CompareAndSwapInt32

适用场景

  • 简单计数器场景
  • 需要高性能的共享变量操作
  • 无需使用锁就能保证操作原子性的场景

代码示例

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var counter int32 = 0
var wg sync.WaitGroup

func increment() {
	for i := 0; i < 1000; i++ {
		atomic.AddInt32(&counter, 1)
	}
	wg.Done()
}

func main() {
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go increment()
	}
	
	wg.Wait()
	fmt.Println("Final counter:", counter)
}

注意事项

  • 仅适用于基本数据类型
  • 对于复杂数据结构,原子操作无法保证完整操作的原子性
  • 适用于简单操作,对于复杂操作仍需使用锁

5. 等待组(WaitGroup)

原理

  • sync.WaitGroup用于等待一组Goroutine完成
  • 有Add()、Done()和Wait()三个方法

适用场景

  • 需要等待多个Goroutine完成后再继续执行的场景
  • 启动多个任务并等待它们完成

代码示例

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int) {
	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 func(id int) {
			defer wg.Done()
			worker(id)
		}(i)
	}
	
	wg.Wait()
	fmt.Println("All workers done")
}

注意事项

  • Add()必须在Start前调用
  • Done()必须在Wait前调用
  • 避免在Wait后再次调用Add或Done

6. Once

原理

  • sync.Once确保某个操作只执行一次
  • 有Do()方法

适用场景

  • 需要确保初始化操作只执行一次的场景
  • 单例模式

代码示例

package main

import (
	"fmt"
	"sync"
)

var once sync.Once
var initialized bool

func initResource() {
	fmt.Println("Initializing resource")
	initialized = true
}

func main() {
	var wg sync.WaitGroup
	
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(initResource)
			fmt.Println("Resource initialized:", initialized)
		}()
	}
	
	wg.Wait()
}

注意事项

  • 适用于需要初始化的单例模式
  • 保证初始化操作只执行一次
  • 适用于全局资源初始化

7. 对象池(Pool)

原理

  • sync.Pool用于对象复用,减少内存分配和GC压力
  • 有Get()和Put()方法
  • 可以通过New函数指定创建新对象的函数

适用场景

  • 需要频繁创建和销毁相同类型对象的场景
  • 例如:HTTP请求处理中的context对象
  • 高性能场景下减少内存分配

代码示例

package main

import (
	"fmt"
	"sync"
)

type Resource struct {
	ID int
}

var pool = sync.Pool{
	New: func() interface{} {
		return &Resource{ID: 0}
	},
}

func main() {
	var wg sync.WaitGroup
	
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			
			// 从池中获取资源
			res := pool.Get().(*Resource)
			res.ID = id
			fmt.Printf("Got resource %d\n", res.ID)
			
			// 使用完后放回池中
			pool.Put(res)
		}(i)
	}
	
	wg.Wait()
}

注意事项

  • 对象池中的对象可能在任何时候被回收
  • 不要将需要持久化状态的对象放入池中
  • 适用于临时对象,不适用于需要长期保存状态的对象

8. 并发安全Map(sync.Map)

原理

  • sync.Map是Go 1.9引入的并发安全的Map
  • 无需额外锁即可安全地进行并发操作
  • 有Store、Load、Delete、Range方法

适用场景

  • 需要并发安全的Map的场景
  • 读多写少的场景
  • 无需额外锁的简单Map操作

代码示例

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	
	// 写入操作
	for i := 0; i < 10; i++ {
		m.Store(i, fmt.Sprintf("Value %d", i))
	}
	
	// 读取操作
	for i := 0; i < 10; i++ {
		if val, ok := m.Load(i); ok {
			fmt.Printf("Key %d: %s\n", i, val)
		}
	}
	
	// 删除操作
	m.Delete(5)
	
	// 遍历操作
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("Key: %v, Value: %v\n", key, value)
		return true
	})
	
	// 读写并发示例
	var wg sync.WaitGroup
	for i := 10; i < 20; i++ {
		wg.Add(1)
		go func(key int) {
			defer wg.Done()
			m.Store(key, fmt.Sprintf("Value %d", key))
		}(i)
	}
	
	wg.Wait()
	
	fmt.Println("After concurrent writes:")
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("Key: %v, Value: %v\n", key, value)
		return true
	})
}

注意事项

  • 适用于读多写少的场景,写操作过多会影响性能
  • 不需要额外的锁,但不能保证所有操作的原子性
  • 适用于简单的键值对存储

总结

同步原语适用场景优点缺点
Mutex读写操作频率相近简单易用读操作多时性能差
RWMutex读多写少读操作并发性能高写操作会阻塞所有读操作
Cond生产者-消费者模型精确控制等待条件需要配合锁使用
Atomic简单计数器高性能,无需锁仅适用于基本数据类型
WaitGroup等待多个Goroutine完成简单易用不能用于任务调度
Once初始化操作确保只执行一次仅适用于初始化
Pool对象复用减少内存分配不能用于持久化对象
sync.Map并发Map无需额外锁写操作过多时性能下降

在实际开发中,应根据具体场景选择合适的同步机制。例如,如果需要高并发读取,应优先考虑RWMutex;如果只是简单的计数,atomic操作更为高效;对于需要对象复用的场景,sync.Pool是理想选择。

最后:并发编程的首要原则是避免共享状态。如果可能,尽量设计为不共享状态,这样可以避免使用同步机制,从而获得更好的性能和可维护性。