Appearance
互斥锁 Mutex
1. 概述
互斥锁(Mutex)是 Go 语言中最基本的同步原语,用于保护共享资源,确保同一时刻只有一个 Goroutine 可以访问共享资源。Mutex 是 sync 包中的一个结构体,提供了简单而有效的并发控制机制。
在整个 Go 语言课程体系中,Mutex 是并发编程的基础组件之一,与 Goroutine、通道一起构成了 Go 语言并发模型的核心。掌握 Mutex 的使用和原理,对于构建可靠、高效的并发系统至关重要。
2. 基本概念
2.1 语法
2.1.1 基本用法
go
import "sync"
// 创建互斥锁
var mu sync.Mutex
// 加锁
mu.Lock()
// 访问共享资源
// ...
// 解锁
mu.Unlock()
// 或者使用 defer 解锁
mu.Lock()
defer mu.Unlock()
// 访问共享资源
// ...2.1.2 示例代码
go
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
counter := 0
var wg sync.WaitGroup
// 1000 个 Goroutine 同时递增计数器
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
counter++
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter) // 应该输出 1000
}2.2 语义
- Lock():获取锁,如果锁已经被其他 Goroutine 持有,则阻塞直到获取到锁。
- Unlock():释放锁,唤醒等待锁的 Goroutine。
- 零值可用:Mutex 的零值是可用的,不需要初始化。
- 不可复制:Mutex 是结构体,不是引用类型,不要复制使用中的 Mutex。
- 不可重入:同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。
2.3 规范
- 命名规范:Mutex 变量通常命名为
mu。 - 使用顺序:总是先加锁,后访问共享资源,最后解锁。
- defer 解锁:使用
defer mu.Unlock()确保即使发生 panic 也能正确解锁。 - 锁粒度:尽量减少持有锁的时间,只在必要时加锁。
- 避免嵌套锁:避免在持有一个锁的同时获取另一个锁,减少死锁的风险。
- 不可复制:通过指针传递 Mutex,避免复制它。
3. 原理深度解析
3.1 Mutex 结构体
Mutex 的底层实现是一个结构体,在 Go 1.18+ 版本中,其结构如下:
go
type Mutex struct {
state int32
sema uint32
}其中:
state:表示锁的状态,包含以下信息:- 锁是否被持有(bit 0)
- 是否有 Goroutine 在等待锁(bit 1)
- 等待锁的 Goroutine 数量(bits 2-30)
sema:信号量,用于唤醒等待的 Goroutine。
3.2 锁的状态转换
Mutex 有两种模式:
- 正常模式:新请求锁的 Goroutine 会先尝试自旋(spin)一段时间,如果获取不到锁则进入等待队列。
- 饥饿模式:当一个 Goroutine 等待锁的时间超过 1ms 时,Mutex 会切换到饥饿模式,此时锁会直接传递给等待时间最长的 Goroutine。
3.3 Lock 方法实现
Lock 方法的主要步骤:
- 快速路径:使用 CAS(Compare-And-Swap)操作尝试获取锁,如果成功则直接返回。
- 自旋:如果锁被占用,且当前 Goroutine 是第一个等待者,且锁的持有者在运行中,则进行短暂的自旋。
- 慢速路径:如果自旋失败,则进入慢速路径,将自己加入等待队列,然后阻塞等待。
- 模式切换:如果等待时间超过 1ms,切换到饥饿模式。
3.4 Unlock 方法实现
Unlock 方法的主要步骤:
- 快速路径:使用 CAS 操作释放锁,如果没有等待者则直接返回。
- 唤醒等待者:如果有等待者,则唤醒队列中的第一个等待者。
- 模式切换:如果锁的持有者是通过正常模式获取的,且等待队列不为空,则切换回正常模式。
3.5 并发安全
Mutex 的所有方法都是并发安全的,使用原子操作来修改状态,确保在多 Goroutine 环境中安全使用。
3.6 内存模型
Mutex 遵循 Go 语言的内存模型,确保以下顺序:
- 在调用
Unlock()之前的所有操作,发生在Lock()返回之后。 - 多个 Goroutine 同时调用
Lock()时,只有一个 Goroutine 会成功获取锁,其他 Goroutine 会等待。
4. 常见错误与踩坑点
4.1 死锁
错误表现:程序卡住,无法继续执行。
产生原因:
- 同一 Goroutine 多次获取同一个 Mutex。
- 多个 Goroutine 循环等待对方释放锁。
- 锁的顺序不一致,导致循环等待。
解决方案:
- 确保每个锁都能正确释放,使用
defer语句。 - 保持一致的锁获取顺序。
- 避免在持有锁时调用可能阻塞的函数。
go
// 错误示例:同一 Goroutine 多次获取同一个 Mutex
func main() {
var mu sync.Mutex
mu.Lock()
mu.Lock() // 死锁
defer mu.Unlock()
defer mu.Unlock()
}
// 错误示例:循环等待
var mu1, mu2 sync.Mutex
func goroutine1() {
mu1.Lock()
defer mu1.Unlock()
time.Sleep(time.Millisecond)
mu2.Lock()
defer mu2.Unlock()
}
func goroutine2() {
mu2.Lock()
defer mu2.Unlock()
time.Sleep(time.Millisecond)
mu1.Lock()
defer mu1.Unlock()
}
// 正确示例:一致的锁顺序
func process() {
// 始终先获取 mu1,再获取 mu2
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 处理逻辑
}4.2 忘记解锁
错误表现:其他 Goroutine 无法获取锁,导致死锁。
产生原因:在获取锁后,没有对应的解锁操作,或者在解锁前发生了 panic。
解决方案:始终使用 defer 语句来确保解锁,即使发生 panic 也能正确解锁。
go
// 错误示例:忘记解锁
func process() {
mu.Lock()
// 处理逻辑
// 忘记调用 mu.Unlock()
}
// 正确示例:使用 defer 解锁
func process() {
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}4.3 锁粒度太大
错误表现:并发性能下降,锁竞争严重。
产生原因:长时间持有锁,执行了很多不需要锁保护的操作。
解决方案:尽量减少持有锁的时间,只在必要时加锁。
go
// 错误示例:锁粒度太大
func process() {
mu.Lock()
defer mu.Unlock()
// 执行耗时操作
time.Sleep(time.Second)
// 修改共享资源
sharedResource = newValue
}
// 正确示例:锁粒度最小化
func process() {
// 执行耗时操作
time.Sleep(time.Second)
// 只在修改共享资源时加锁
mu.Lock()
sharedResource = newValue
mu.Unlock()
}4.4 复制 Mutex
错误表现:复制的 Mutex 与原 Mutex 状态不同步,导致不可预期的行为。
产生原因:Mutex 是结构体,不是引用类型,复制后会创建一个新的实例,与原实例状态无关。
解决方案:通过指针传递 Mutex,而不是复制它。
go
// 错误示例:复制 Mutex
func worker(mu sync.Mutex) { // 复制 Mutex
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}
// 正确示例:通过指针传递
func worker(mu *sync.Mutex) { // 通过指针传递
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}4.5 锁的使用不当导致性能问题
错误表现:程序运行缓慢,并发性能差。
产生原因:
- 锁的粒度太大,导致锁竞争严重。
- 在持有锁时执行 I/O 操作或其他阻塞操作。
- 不必要的锁使用,如对只读操作加锁。
解决方案:
- 最小化锁的粒度,只在必要时加锁。
- 避免在持有锁时执行阻塞操作。
- 对于读多写少的场景,使用 RWMutex 提高并发性能。
go
// 错误示例:在持有锁时执行 I/O 操作
func process() {
mu.Lock()
defer mu.Unlock()
// 执行 I/O 操作
data, err := ioutil.ReadFile("file.txt")
if err != nil {
return
}
// 处理数据
sharedResource = processData(data)
}
// 正确示例:先执行 I/O 操作,再获取锁
func process() {
// 执行 I/O 操作
data, err := ioutil.ReadFile("file.txt")
if err != nil {
return
}
// 处理数据
processedData := processData(data)
// 只在修改共享资源时加锁
mu.Lock()
sharedResource = processedData
mu.Unlock()
}5. 常见应用场景
5.1 保护共享变量
场景描述:多个 Goroutine 需要访问和修改同一个共享变量,如计数器、配置等。
使用方法:使用 Mutex 保护共享变量,确保同一时刻只有一个 Goroutine 可以修改它。
示例代码:
go
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
counter := 0
var wg sync.WaitGroup
// 1000 个 Goroutine 同时递增计数器
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
counter++
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter) // 应该输出 1000
}5.2 保护共享数据结构
场景描述:多个 Goroutine 需要访问和修改同一个共享数据结构,如 map、slice 等。
使用方法:使用 Mutex 保护共享数据结构,确保同一时刻只有一个 Goroutine 可以修改它。
示例代码:
go
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
data := make(map[string]int)
var wg sync.WaitGroup
// 10 个 Goroutine 同时修改 map
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", id)
mu.Lock()
defer mu.Unlock()
data[key] = id
fmt.Printf("Added %s: %d\n", key, id)
}(i)
}
wg.Wait()
fmt.Println("Final data:")
for k, v := range data {
fmt.Printf("%s: %d\n", k, v)
}
}5.3 实现线程安全的结构体
场景描述:需要实现一个线程安全的结构体,支持并发访问和修改。
使用方法:在结构体中嵌入 Mutex,在方法中使用 Mutex 保护共享资源。
示例代码:
go
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
// 1000 个 Goroutine 同时递增计数器
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter.Get()) // 应该输出 1000
}5.4 实现临界区
场景描述:需要确保一段代码在同一时刻只有一个 Goroutine 执行,如初始化操作、资源清理等。
使用方法:使用 Mutex 保护临界区,确保同一时刻只有一个 Goroutine 执行这段代码。
示例代码:
go
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var initialized bool
var wg sync.WaitGroup
// 10 个 Goroutine 同时尝试初始化
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
if !initialized {
fmt.Printf("Goroutine %d: initializing\n", id)
initialized = true
fmt.Printf("Goroutine %d: initialization completed\n", id)
} else {
fmt.Printf("Goroutine %d: already initialized\n", id)
}
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}5.5 实现单例模式
场景描述:需要创建一个全局唯一的实例,确保只初始化一次。
使用方法:使用 Mutex 保护单例的初始化过程,确保只初始化一次。
示例代码:
go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var (
instance *Singleton
mu sync.Mutex
)
func GetInstance() *Singleton {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{data: "initialized"}
fmt.Println("Singleton initialized")
}
return instance
}
// 优化版本:双重检查锁定
func GetInstanceOptimized() *Singleton {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{data: "initialized"}
fmt.Println("Singleton initialized")
}
}
return instance
}
func main() {
var wg sync.WaitGroup
// 多个 Goroutine 同时获取单例实例
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
s := GetInstanceOptimized()
fmt.Printf("Goroutine %d: %s\n", id, s.data)
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}6. 企业级进阶应用场景
6.1 并发安全的配置管理
场景描述:在大型应用中,需要管理全局配置,支持并发读写和热更新。
使用方法:使用 Mutex 保护配置,确保配置的一致性。
示例代码:
go
package config
import (
"sync"
)
type Config struct {
Server struct {
Host string
Port int
}
Database struct {
DSN string
}
// 其他配置项
}
var (
config Config
mu sync.Mutex
)
func GetConfig() Config {
mu.Lock()
defer mu.Unlock()
return config
}
func UpdateConfig(newConfig Config) {
mu.Lock()
defer mu.Unlock()
config = newConfig
}
// 热更新配置
func ReloadConfig() error {
// 从文件或环境变量加载配置
newConfig, err := loadConfig()
if err != nil {
return err
}
UpdateConfig(newConfig)
return nil
}6.2 并发安全的缓存
场景描述:在高并发系统中,需要一个线程安全的缓存,支持并发读写。
使用方法:使用 Mutex 保护缓存,确保缓存的一致性。
示例代码:
go
package cache
import (
"sync"
"time"
)
type Item struct {
Value interface{}
Expiration int64
}
type Cache struct {
items map[string]Item
mu sync.Mutex
}
func NewCache() *Cache {
c := &Cache{
items: make(map[string]Item),
}
// 启动清理过期项的 Goroutine
go c.cleanup()
return c
}
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
var exp int64
if expiration > 0 {
exp = time.Now().Add(expiration).UnixNano()
}
c.items[key] = Item{
Value: value,
Expiration: exp,
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, found := c.items[key]
if !found {
return nil, false
}
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
delete(c.items, key)
return nil, false
}
return item.Value, true
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache) cleanup() {
for {
time.Sleep(time.Minute)
c.mu.Lock()
now := time.Now().UnixNano()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}6.3 并发安全的队列
场景描述:在生产者-消费者模式中,需要一个线程安全的队列,支持并发入队和出队。
使用方法:使用 Mutex 保护队列,确保队列的一致性。
示例代码:
go
package queue
import (
"sync"
)
type Queue struct {
items []interface{}
mu sync.Mutex
}
func NewQueue() *Queue {
return &Queue{
items: make([]interface{}, 0),
}
}
func (q *Queue) Enqueue(item interface{}) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
}
func (q *Queue) Dequeue() (interface{}, bool) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.items) == 0 {
return nil, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
func (q *Queue) Size() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.items)
}6.4 并发安全的计数器
场景描述:在分布式系统中,需要一个高并发的计数器,支持原子操作。
使用方法:使用 Mutex 保护计数器,确保计数器的一致性。
示例代码:
go
package counter
import (
"sync"
)
type Counter struct {
value int64
mu sync.Mutex
}
func NewCounter() *Counter {
return &Counter{value: 0}
}
func (c *Counter) Increment() int64 {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
return c.value
}
func (c *Counter) Decrement() int64 {
c.mu.Lock()
defer c.mu.Unlock()
c.value--
return c.value
}
func (c *Counter) Get() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func (c *Counter) Set(value int64) {
c.mu.Lock()
defer c.mu.Unlock()
c.value = value
}6.5 并发安全的连接池
场景描述:在高并发系统中,需要管理数据库连接、网络连接等资源,避免频繁创建和销毁连接。
使用方法:使用 Mutex 保护连接池,确保连接的安全获取和归还。
示例代码:
go
package pool
import (
"sync"
)
type Connection interface {
Close() error
}
type Pool struct {
connections chan Connection
mu sync.Mutex
closed bool
}
func NewPool(size int, factory func() (Connection, error)) (*Pool, error) {
pool := &Pool{
connections: make(chan Connection, size),
closed: false,
}
// 预创建连接
for i := 0; i < size; i++ {
conn, err := factory()
if err != nil {
return nil, err
}
pool.connections <- conn
}
return pool, nil
}
func (p *Pool) Get() (Connection, error) {
p.mu.Lock()
if p.closed {
p.mu.Unlock()
return nil, fmt.Errorf("pool is closed")
}
p.mu.Unlock()
conn := <-p.connections
return conn, nil
}
func (p *Pool) Put(conn Connection) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return conn.Close()
}
select {
case p.connections <- conn:
return nil
default:
// 连接池已满,关闭多余的连接
return conn.Close()
}
}
func (p *Pool) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
close(p.connections)
for conn := range p.connections {
conn.Close()
}
return nil
}
func (p *Pool) Size() int {
p.mu.Lock()
defer p.mu.Unlock()
return len(p.connections)
}7. 行业最佳实践
7.1 始终使用 defer 解锁
实践内容:使用 defer 语句确保锁的释放。
推荐理由:defer 语句可以确保即使发生 panic,锁也能正确释放,避免死锁。
示例:
go
func process() {
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}7.2 最小化锁的粒度
实践内容:只对需要保护的共享资源加锁,避免对整个函数加锁。
推荐理由:最小化锁的粒度可以提高并发性能,减少锁竞争。
示例:
go
// 错误示例:对整个函数加锁
func process() {
mu.Lock()
defer mu.Unlock()
// 执行不需要锁的操作
fmt.Println("Processing...")
// 修改共享资源
sharedResource = newValue
}
// 正确示例:只对共享资源加锁
func process() {
// 执行不需要锁的操作
fmt.Println("Processing...")
// 只对共享资源加锁
mu.Lock()
sharedResource = newValue
mu.Unlock()
}7.3 避免在持有锁时执行阻塞操作
实践内容:避免在持有锁时执行 I/O 操作、网络请求等阻塞操作。
推荐理由:在持有锁时执行阻塞操作会增加锁的持有时间,降低并发性能,增加死锁的风险。
示例:
go
// 错误示例:在持有锁时执行 I/O 操作
func process() {
mu.Lock()
defer mu.Unlock()
// 执行 I/O 操作
data, err := ioutil.ReadFile("file.txt")
if err != nil {
return
}
// 处理数据
sharedResource = processData(data)
}
// 正确示例:先执行 I/O 操作,再获取锁
func process() {
// 执行 I/O 操作
data, err := ioutil.ReadFile("file.txt")
if err != nil {
return
}
// 处理数据
processedData := processData(data)
// 只在修改共享资源时加锁
mu.Lock()
sharedResource = processedData
mu.Unlock()
}7.4 使用双重检查锁定模式
实践内容:对于单例模式等场景,使用双重检查锁定模式提高性能。
推荐理由:双重检查锁定模式可以减少锁的竞争,提高性能。
示例:
go
func GetInstance() *Singleton {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{data: "initialized"}
}
}
return instance
}7.5 保持一致的锁获取顺序
实践内容:当需要获取多个锁时,保持一致的获取顺序。
推荐理由:保持一致的锁获取顺序可以避免死锁。
示例:
go
// 正确示例:保持一致的锁顺序
func process() {
// 始终先获取 mu1,再获取 mu2
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 处理逻辑
}7.6 避免嵌套锁
实践内容:避免在持有一个锁的同时获取另一个锁。
推荐理由:嵌套锁容易导致死锁,尤其是当多个 Goroutine 以不同的顺序获取锁时。
示例:
go
// 错误示例:嵌套锁
func process() {
mu1.Lock()
defer mu1.Unlock()
// ...
mu2.Lock()
defer mu2.Unlock()
// ...
}
// 正确示例:避免嵌套锁
func process() {
// 先获取所有需要的锁
mu1.Lock()
mu2.Lock()
// 处理逻辑
mu2.Unlock()
mu1.Unlock()
}7.7 监控锁的使用
实践内容:在生产环境中监控锁的使用情况,如锁的持有时间、竞争情况等。
推荐理由:监控锁的使用情况可以帮助发现潜在的性能问题和死锁风险。
示例:
go
// 使用监控工具监控锁的使用
func process() {
start := time.Now()
mu.Lock()
defer func() {
mu.Unlock()
duration := time.Since(start)
metrics.RecordLockHoldTime("process", duration)
}()
// 处理逻辑
}7.8 选择合适的同步原语
实践内容:根据具体场景选择合适的同步原语。
推荐理由:不同的同步原语有不同的适用场景,选择合适的同步原语可以提高性能和代码可读性。
示例:
- 对于读多写少的场景,使用 RWMutex。
- 对于简单的计数器,使用 atomic 包。
- 对于需要等待条件满足的场景,使用 Cond。
- 对于需要等待多个 Goroutine 完成的场景,使用 WaitGroup。
8. 常见问题答疑(FAQ)
8.1 Mutex 和 RWMutex 有什么区别?
问题描述:Mutex 和 RWMutex 都是同步原语,它们有什么区别?
回答内容:
- Mutex:互斥锁,确保同一时刻只有一个 Goroutine 可以访问共享资源。
- RWMutex:读写锁,允许多个读操作同时进行,但写操作会阻塞所有读写操作。
- 使用场景:
- 当读写比例相近时,使用 Mutex。
- 当读操作远多于写操作时,使用 RWMutex 可以提高并发性能。
示例代码:
go
// 使用 Mutex
var mu sync.Mutex
// 使用 RWMutex
var rwmu sync.RWMutex8.2 如何避免死锁?
问题描述:如何避免死锁?
回答内容:
- 始终使用
defer语句确保锁的释放。 - 保持一致的锁获取顺序。
- 避免在持有锁时调用可能阻塞的函数。
- 避免嵌套锁,或者确保以相同的顺序获取锁。
- 使用超时机制,避免无限等待。
- 定期检查代码中的死锁风险。
示例代码:
go
// 避免死锁:保持一致的锁顺序
func process() {
// 始终先获取 mu1,再获取 mu2
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 处理逻辑
}8.3 如何提高 Mutex 的性能?
问题描述:如何提高 Mutex 的性能?
回答内容:
- 最小化锁的粒度,只在必要时加锁。
- 避免在持有锁时执行阻塞操作。
- 使用双重检查锁定模式减少锁的竞争。
- 对于读多写少的场景,使用 RWMutex。
- 对于简单的计数器,使用 atomic 包。
- 合理设计并发模型,减少锁的使用。
示例代码:
go
// 提高性能:最小化锁的粒度
func process() {
// 执行不需要锁的操作
data := computeData()
// 只在修改共享资源时加锁
mu.Lock()
sharedResource = data
mu.Unlock()
}8.4 Mutex 是可重入的吗?
问题描述:Mutex 是可重入的吗?
回答内容:
- 不是,Mutex 不是可重入的。
- 同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。
- 如果需要可重入的锁,可以使用 sync.Mutex 的包装实现,或者使用其他同步原语。
示例代码:
go
// 错误示例:Mutex 不可重入
func main() {
var mu sync.Mutex
mu.Lock()
mu.Lock() // 死锁
defer mu.Unlock()
defer mu.Unlock()
}8.5 如何处理 Mutex 的复制问题?
问题描述:如何处理 Mutex 的复制问题?
回答内容:
- Mutex 是结构体,不是引用类型,复制后会创建一个新的实例,与原实例状态无关。
- 避免复制使用中的 Mutex,通过指针传递 Mutex。
- 在结构体中嵌入 Mutex 时,注意不要复制整个结构体。
示例代码:
go
// 错误示例:复制 Mutex
func worker(mu sync.Mutex) { // 复制 Mutex
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}
// 正确示例:通过指针传递
func worker(mu *sync.Mutex) { // 通过指针传递
mu.Lock()
defer mu.Unlock()
// 处理逻辑
}8.6 Mutex 和通道有什么区别?
问题描述:Mutex 和通道都是 Go 语言中用于并发控制的工具,它们有什么区别?
回答内容:
- Mutex:互斥锁,用于保护共享资源,确保同一时刻只有一个 Goroutine 可以访问共享资源。
- 通道:用于 Goroutine 间的通信,可以传递数据和信号。
- 使用场景:
- 当需要保护共享资源时,使用 Mutex。
- 当需要在 Goroutine 间传递数据或信号时,使用通道。
- 当需要更复杂的并发控制时,结合使用 Mutex 和通道。
示例代码:
go
// 使用 Mutex 保护共享资源
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
// 使用通道进行通信
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch9. 实战练习
9.1 基础练习:并发安全的计数器
题目:使用 Mutex 实现一个并发安全的计数器,支持递增、递减和获取操作。
解题思路:
- 使用 Mutex 保护计数器变量。
- 实现 Increment、Decrement 和 Get 方法。
- 测试多个 Goroutine 同时操作计数器。
常见误区:
- 忘记使用
defer解锁,导致死锁。 - 锁的粒度太大,导致性能下降。
分步提示:
- 定义计数器结构体,包含计数器值和 Mutex。
- 实现 Increment 方法,使用 Mutex 保护计数器的递增操作。
- 实现 Decrement 方法,使用 Mutex 保护计数器的递减操作。
- 实现 Get 方法,使用 Mutex 保护计数器的读取操作。
- 启动多个 Goroutine 同时操作计数器。
- 验证计数器的最终值是否正确。
参考代码:
go
package main
import (
"fmt"
"sync"
)
type Counter struct {
value int
mu sync.Mutex
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Decrement() {
c.mu.Lock()
defer c.mu.Unlock()
c.value--
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
// 1000 个 Goroutine 同时递增计数器
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
// 500 个 Goroutine 同时递减计数器
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Decrement()
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter.Get()) // 应该输出 500
}9.2 进阶练习:并发安全的缓存
题目:使用 Mutex 实现一个并发安全的缓存,支持设置、获取和删除操作,以及过期时间。
解题思路:
- 使用 Mutex 保护缓存映射。
- 实现 Set、Get 和 Delete 方法。
- 支持设置过期时间,定期清理过期项。
- 测试多个 Goroutine 同时访问缓存。
常见误区:
- 在读操作时使用写锁定,导致性能下降。
- 没有正确处理并发访问,导致竞态条件。
- 没有清理过期项,导致缓存膨胀。
分步提示:
- 定义缓存项结构体,包含值和过期时间。
- 定义缓存结构体,包含映射和 Mutex。
- 实现 Set 方法,使用 Mutex 保护缓存的设置操作。
- 实现 Get 方法,使用 Mutex 保护缓存的获取操作,并检查过期时间。
- 实现 Delete 方法,使用 Mutex 保护缓存的删除操作。
- 启动一个 Goroutine 定期清理过期项。
- 测试多个 Goroutine 同时访问缓存。
参考代码:
go
package main
import (
"fmt"
"sync"
"time"
)
type Item struct {
Value interface{}
Expiration int64
}
type Cache struct {
items map[string]Item
mu sync.Mutex
}
func NewCache() *Cache {
c := &Cache{
items: make(map[string]Item),
}
// 启动清理过期项的 Goroutine
go c.cleanup()
return c
}
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
var exp int64
if expiration > 0 {
exp = time.Now().Add(expiration).UnixNano()
}
c.items[key] = Item{
Value: value,
Expiration: exp,
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, found := c.items[key]
if !found {
return nil, false
}
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
delete(c.items, key)
return nil, false
}
return item.Value, true
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache) cleanup() {
for {
time.Sleep(time.Minute)
c.mu.Lock()
now := time.Now().UnixNano()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}
func main() {
cache := NewCache()
var wg sync.WaitGroup
// 启动 10 个写 Goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
key := fmt.Sprintf("key-%d-%d", id, j)
value := fmt.Sprintf("value-%d-%d", id, j)
cache.Set(key, value, time.Hour)
time.Sleep(time.Millisecond)
}
}(i)
}
// 启动 100 个读 Goroutine
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
key := fmt.Sprintf("key-%d-%d", id%10, j)
_, _ = cache.Get(key)
time.Sleep(time.Millisecond)
}
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
fmt.Printf("Cache size: %d\n", len(cache.items))
}9.3 挑战练习:生产者-消费者模式
题目:使用 Mutex 和 Cond 实现一个生产者-消费者模式,支持多个生产者和多个消费者。
解题思路:
- 使用 Mutex 保护队列。
- 使用 Cond 等待队列满或空的条件。
- 实现生产者,在队列满时等待,在生产后通知消费者。
- 实现消费者,在队列空时等待,在消费后通知生产者。
- 测试多个生产者和多个消费者的并发场景。
常见误区:
- 没有在循环中检查条件,导致虚假唤醒。
- 没有正确获取和释放锁,导致死锁。
- 信号丢失,即先发送信号后等待。
分步提示:
- 定义队列结构体,包含切片、Mutex 和 Cond。
- 实现 Enqueue 方法,在队列满时等待,在生产后通知消费者。
- 实现 Dequeue 方法,在队列空时等待,在消费后通知生产者。
- 启动多个生产者和多个消费者。
- 测试并发场景下的正确性。
参考代码:
go
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
items []int
mu sync.Mutex
cond *sync.Cond
size int
}
func NewQueue(size int) *Queue {
q := &Queue{
items: make([]int, 0, size),
size: size,
}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Enqueue(item int) {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) >= q.size {
fmt.Println("Queue full, waiting to enqueue")
q.cond.Wait()
}
q.items = append(q.items, item)
fmt.Printf("Enqueued: %d, queue size: %d\n", item, len(q.items))
q.cond.Broadcast()
}
func (q *Queue) Dequeue() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
fmt.Println("Queue empty, waiting to dequeue")
q.cond.Wait()
}
item := q.items[0]
q.items = q.items[1:]
fmt.Printf("Dequeued: %d, queue size: %d\n", item, len(q.items))
q.cond.Broadcast()
return item
}
func main() {
queue := NewQueue(5)
var wg sync.WaitGroup
// 启动 3 个生产者
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
item := id*10 + j
queue.Enqueue(item)
time.Sleep(time.Millisecond * 100)
}
}(i)
}
// 启动 2 个消费者
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 7; j++ {
item := queue.Dequeue()
fmt.Printf("Consumer %d: processed %d\n", id, item)
time.Sleep(time.Millisecond * 150)
}
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}10. 知识点总结
10.1 核心要点
- Mutex:互斥锁,确保同一时刻只有一个 Goroutine 可以访问共享资源。
- 基本用法:使用
Lock()加锁,Unlock()解锁,或者使用defer Unlock()确保解锁。 - 并发安全:Mutex 的所有方法都是并发安全的,使用原子操作来修改状态。
- 零值可用:Mutex 的零值是可用的,不需要初始化。
- 不可复制:Mutex 是结构体,不是引用类型,不要复制使用中的 Mutex。
- 不可重入:同一个 Goroutine 不能多次获取同一个 Mutex,否则会导致死锁。
- 性能优化:最小化锁的粒度,避免在持有锁时执行阻塞操作,使用双重检查锁定模式等。
10.2 易错点回顾
- 死锁:多个 Goroutine 互相等待对方释放锁,导致程序无法继续执行。
- 忘记解锁:在获取锁后,没有对应的解锁操作,导致其他 Goroutine 无法获取锁。
- 锁粒度太大:长时间持有锁,执行了很多不需要锁保护的操作,导致并发性能下降。
- 复制 Mutex:复制使用中的 Mutex,导致状态不同步。
- 在持有锁时执行阻塞操作:增加锁的持有时间,降低并发性能。
- 嵌套锁:在持有一个锁的同时获取另一个锁,增加死锁的风险。
11. 拓展参考资料
11.1 官方文档链接
11.2 进阶学习路径建议
- 并发模式:学习常见的并发模式,如生产者-消费者、工作池、扇入扇出等。
- 通道:深入学习通道的使用和通道模式,理解 CSP 并发模型。
- Context:学习 Context 的使用,实现更复杂的并发控制,如超时控制、取消操作等。
- 性能优化:学习并发性能优化技巧,如减少锁竞争、使用原子操作、优化缓存等。
- 分布式系统:学习分布式系统中的并发控制,如分布式锁、一致性协议等。
11.3 相关资源
- Go 语言实战:介绍 Go 语言的并发编程和同步原语。
- Go 并发编程实战:深入讲解 Go 语言的并发编程技术。
- Go 语言设计与实现:深入分析 Go 语言的内部实现,包括 Mutex 的底层原理。
- Concurrency in Go:英文书籍,详细介绍 Go 语言的并发编程。
- The Go Memory Model:Go 语言内存模型,解释了并发操作的内存顺序。
