【容易踩坑golang 锁相关知识

2023年 8月 18日 21.7k 0

1 golang中锁的作用

在 Golang 中,锁的作用是用于保护共享资源,确保在多个 goroutine 并发访问时的数据一致性和正确性。当多个 goroutine 同时访问共享资源时,可能会出现竞态条件(race condition),导致数据错误或不一致。

通过使用锁,可以实现以下目标:

  • 互斥访问:锁可以确保同一时间只有一个 goroutine 可以访问共享资源,避免并发访问导致的数据竞争。
  • 数据同步:锁可以用于在不同 goroutine 之间进行同步,确保在一个 goroutine 修改共享资源时,其他 goroutine 不会读取或修改该资源。
  • 顺序保证:锁可以用于实现临界区,确保多个 goroutine 按照特定的顺序访问共享资源,避免出现不确定的结果。
  • 防止死锁:通过合理使用锁的机制,可以避免出现死锁情况,即多个 goroutine 互相等待对方释放锁而无法继续执行的情况。
  • 总而言之,锁在 Golang 中起到了保护共享资源、同步多个 goroutine、确保数据一致性和避免竞态条件等重要作用。

    2 golang中几种锁的实现

    Golang 提供了几种锁的实现,包括:

  • sync.Mutex:最基本的互斥锁,用于保护临界区,只能被同一个 goroutine 获取和释放。
  • sync.RWMutex:读写锁,允许多个 goroutine 同时读取共享资源,但只允许一个 goroutine 写入共享资源。
  • sync.WaitGroup:用于等待一组 goroutine 完成执行,主 goroutine 可以等待所有 goroutine 完成后再继续执行。
  • sync.Once:确保只执行一次的锁,常用于初始化操作。
  • sync.Cond:条件变量,可以让 goroutine 在某个条件满足时等待或被唤醒。
  • sync.Pool:对象池,用于复用临时对象,避免频繁的内存分配和释放。
  • 这些锁都是在 sync 包中定义的,可以根据实际需求选择合适的锁来保护共享资源。

    3 sync.Mutex

    sync.Mutex 是 Golang 中最基本的互斥锁。它通过两个方法来实现对共享资源的保护:LockUnlock。在 Lock 方法调用之后,其他 goroutine 将被阻塞,直到当前 goroutine 调用 Unlock 方法释放锁。

    下面是一个相对复杂的例子,展示了如何使用 sync.Mutex 来保护一个共享资源,确保并发访问的安全性:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    type Counter struct {
    	mu    sync.Mutex // 互斥锁
    	count int        // 共享资源
    }
    
    func (c *Counter) Increment() {
    	c.mu.Lock()         // 加锁
    	defer c.mu.Unlock() // 在函数执行完毕后释放锁
    	c.count++           // 对共享资源进行操作
    }
    
    func (c *Counter) Decrement() {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	c.count--
    }
    
    func (c *Counter) GetCount() int {
    	c.mu.Lock()
    	defer c.mu.Unlock()
    	return c.count
    }
    
    func main() {
    	counter := &Counter{}
    
    	// 启动多个 goroutine 并发地对计数器进行操作
    	for i := 0; i < 10; i++ {
    		go func() {
    			counter.Increment()             // 对计数器进行增加操作
    			time.Sleep(time.Millisecond * 100) // 模拟耗时操作
    			counter.Decrement()             // 对计数器进行减少操作
    		}()
    	}
    
    	time.Sleep(time.Second)
    
    	fmt.Println("Final Count:", counter.GetCount()) // 获取最终的计数器值并输出
    }
    

    在上述例子中,Counter 结构体包含一个 sync.Mutex 类型的字段 mu 和一个整型字段 count。Increment、Decrement 和 GetCount 方法都使用了 sync.Mutex 来保护对 count 字段的访问。

    在 main 函数中,启动了 10 个 goroutine 并发地对计数器进行增加和减少操作。通过使用 sync.Mutex,确保了对 count 字段的访问是互斥的,避免了竞态条件的发生。

    最后,通过调用 GetCount 方法获取最终的计数器值,并输出到控制台。由于使用了 sync.Mutex,保证了并发访问的安全性,所以最终输出的计数器值是正确的。

    // 最终输出结果为:
    Final Count: 0
    

    4 sync.RWMutex

    sync.RWMutex 是 Golang 中的读写锁,它允许多个 goroutine 并发地读取共享资源,但只允许一个 goroutine 写入共享资源。这种机制可以提高并发读取的效率,同时保证写入操作的原子性和数据一致性。

    下面是一个相对复杂的例子,展示了如何使用 sync.RWMutex 来保护一个缓存结构,实现读写分离的并发访问:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    type Cache struct {
    	mu    sync.RWMutex
    	items map[string]string
    }
    
    func (c *Cache) Get(key string) (string, bool) {
    	c.mu.RLock()         // 获取读锁
    	defer c.mu.RUnlock() // 在函数执行完毕后释放读锁
    
    	value, ok := c.items[key]
    	return value, ok
    }
    
    func (c *Cache) Set(key string, value string) {
    	c.mu.Lock()         // 获取写锁
    	defer c.mu.Unlock() // 在函数执行完毕后释放写锁
    
    	c.items[key] = value
    }
    
    func main() {
    	cache := &Cache{
    		items: make(map[string]string),
    	}
    
    	// 启动多个 goroutine 并发地对缓存进行读写操作
    	for i := 0; i < 10; i++ {
    		go func() {
    			cache.Set("key", "value") // 写入缓存
    			time.Sleep(time.Millisecond * 100)
    			value, _ := cache.Get("key") // 读取缓存
    			fmt.Println("Value:", value)
    		}()
    	}
    
    	time.Sleep(time.Second)
    }
    

    在上述例子中,Cache 结构体包含一个 sync.RWMutex 类型的字段 mu 和一个 map 类型的字段 items,用于存储缓存数据。

    Get 方法使用了 sync.RWMutex 的 RLock 方法获取读锁,允许多个 goroutine 并发地读取缓存数据。Set 方法使用了 sync.RWMutex 的 Lock 方法获取写锁,确保只有一个 goroutine 能够写入缓存数据。

    在 main 函数中,启动了 10 个 goroutine 并发地对缓存进行读写操作。通过使用 sync.RWMutex,实现了读写分离的并发访问,提高了读取操作的并发性能。

    最后,通过调用 Get 方法读取缓存数据,并将其输出到控制台。由于使用了 sync.RWMutex,保证了并发访问的安全性,所以最终输出的缓存值是正确的。

    请注意,上述代码中没有使用 defer 语句来释放读锁,这是因为读锁在函数执行完毕后会自动释放,不需要手动释放。

    // 执行结果
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    Value: value
    

    5 sync.WaitGroup

    sync.WaitGroup 是 Golang 中的等待组,它用于等待一组 goroutine 的完成。当我们启动多个 goroutine 并发执行任务时,可以使用 sync.WaitGroup 来等待所有 goroutine 完成后再继续执行后续操作。

    下面是一个相对复杂的例子,展示了如何使用 sync.WaitGroup 来等待一组 goroutine 完成后再输出结果:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func main() {
    	var wg sync.WaitGroup
    
    	for i := 0; i < 5; i++ {
    		wg.Add(1) // 增加等待组的计数器
    
    		go func(index int) {
    			defer wg.Done() // 减少等待组的计数器
    
    			time.Sleep(time.Second * time.Duration(index))
    			fmt.Println("Goroutine", index, "finished")
    		}(i)
    	}
    
    	wg.Wait() // 等待所有 goroutine 完成
    
    	fmt.Println("All goroutines finished")
    }
    

    在上述例子中,我们创建了一个 sync.WaitGroup 类型的变量 wg,并在 for 循环中使用 wg.Add(1) 增加等待组的计数器。然后,我们启动了 5 个 goroutine,并在每个 goroutine 中使用 defer wg.Done() 减少等待组的计数器。

    在每个 goroutine 中,我们使用 time.Sleep 模拟一些耗时的操作,并输出当前 goroutine 的编号。

    最后,我们调用 wg.Wait() 来等待所有 goroutine 完成。这会阻塞当前的主 goroutine,直到等待组的计数器变为 0。

    当所有 goroutine 完成后,我们输出 "All goroutines finished"。

    通过使用 sync.WaitGroup,我们可以实现等待一组 goroutine 的完成,并确保在所有 goroutine 完成后再进行后续操作。这在并发编程中非常有用,特别是当我们需要等待多个 goroutine 并发执行任务,并在它们完成后进行汇总或输出结果时。

    6 sync.Once

    sync.Once 是 Golang 中的一个原子操作,它可以确保某个函数只被执行一次。sync.Once 类型有一个 Do 方法,该方法接受一个函数作为参数,并保证这个函数只会被执行一次,无论 Do 方法被调用多少次。

    下面是一个相对复杂的例子,展示了如何使用 sync.Once 来确保某个函数只被执行一次:

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    func main() {
    	var once sync.Once
    
    	for i := 0; i < 5; i++ {
    		once.Do(func() {
    			fmt.Println("Initialization")
    		})
    
    		fmt.Println("Operation", i)
    	}
    }
    

    在上述例子中,我们创建了一个 sync.Once 类型的变量 once,并在 for 循环中使用 once.Do 方法来确保传入的函数只会被执行一次。

    在每次循环中,我们调用 once.Do 方法,并传入一个匿名函数。在第一次调用时,匿名函数会被执行,输出 "Initialization"。在后续的调用中,匿名函数不会再被执行。

    除了匿名函数,我们还在循环中输出了 "Operation" 和当前循环的索引。

    运行上述代码,输出如下:

    // 执行结果
    Initialization
    Operation 0
    Operation 1
    Operation 2
    Operation 3
    Operation 4
    

    通过使用 sync.Once,我们可以确保某个函数只被执行一次。这在一些需要进行初始化操作的场景中非常有用,比如数据库连接、全局配置等。无论调用多少次,只会执行一次初始化操作,避免了重复初始化的问题。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论