Golang中Mutex的使用 - 为什么代码会锁住

Golang中Mutex的使用 - 为什么代码会锁住 运行下面的代码会产生死锁。正如解释的那样:

因为 get() 方法会获取锁,然后调用 count() 方法,而 count() 方法在 set() 方法解锁之前也会获取锁

实际上不太清楚的是,为什么一个方法在对象上获取锁,然后这个方法调用另一个也在同一对象上获取锁的方法会导致死锁。这是什么机制?而且 count() 方法在 set() 方法解锁之前也会获取锁 有什么问题?

即使 count() 方法也会获取锁,它也会在 set() 完成之前释放锁。那么为什么这两个方法不能处理这个问题呢?

package main

    import (
    	"fmt"
    	"sync"
    )

type DataStore struct {
	sync.Mutex
	cache      map[string]string
}

func New() *DataStore {
	return &DataStore{
		cache: make(map[string]string),
	}
}

func (ds *DataStore) set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.cache[key] = value
}

func (ds *DataStore) get(key string) string {
	ds.Lock()
	defer ds.Unlock()
	if ds.count() > 0 {
		item := ds.cache[key]
		return item
	}
	return ""
}

func (ds *DataStore) count() int {
	ds.Lock()
	defer ds.Unlock()
	return len(ds.cache)
}

func main() {
	store := New()
	store.set("Go", "Lang")
	result := store.get("Go")
	fmt.Println(result)
}

更多关于Golang中Mutex的使用 - 为什么代码会锁住的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

对映射的并发访问可能导致程序崩溃,因此必须通过加锁机制(特别是写入操作)来保护它们。

更多关于Golang中Mutex的使用 - 为什么代码会锁住的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


GreyShatter:

Go不支持递归互斥锁。

所以正是因为使用了不被支持的递归互斥锁才导致了死锁。

在你这种情况下,如果只有一个互斥锁,我认为在从其他方法访问资源(映射)之前必须先解锁。我认为在同一互斥锁上使用嵌套锁不是一个好主意。

不要过度设计: 将 get() 函数内部的 ds.count() 调用替换为 len(ds.cache) 以避免破坏锁。

或者直接删除整个 if ds.count() > 0 代码块,因为你不需要先检查长度。 直接返回 ds.cache[key] 即可。

我同意@j-forster针对您特定情况的看法,但如果您想扩展代码以包含其他可能创建递归锁的方法,或许应该采用其他方式,比如封装标准锁定/解锁操作并设置信号量,以避免在已经锁定映射的情况下再次锁定……或采取其他适当措施。

kync:

您在这种情况下不需要使用递归互斥锁,sync.RWMutex 将是解决此问题的方案。

RWMutex 在这里并不是解决方案。根据文档

它不应用于递归读锁定;被阻塞的 Lock 调用会阻止新的读取器获取锁。请参阅 RWMutex 类型的文档。

编辑: 哦,不对。是我看错了。是的,您在这种情况下是对的,抱歉。

嗯,确实如此,但我的问题角度略有不同。

据我理解,第二个方法 count() 无法访问 ds 数据存储,因为它已被 get 方法中使用的互斥锁锁定。这就是为什么作者在说以下这段话时意图不明确的原因:

count() 方法将在 set() 方法解锁之前再获取一个锁

如果 ds 已经被锁定,count() 方法是否还能再获取一个锁?

func main() {
    fmt.Println("hello world")
}

在你的场景中不需要使用递归互斥锁,sync.RWMutex 可以解决这个问题。读取时使用 RLock,写入时使用 Lock。

func (ds *DataStore) set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.cache[key] = value
}

func (ds *DataStore) get(key string) string {
	ds.RLock()
	defer ds.RUnlock()
	if ds.count() > 0 {
		item := ds.cache[key]
		return item
	}
	return ""
}

func (ds *DataStore) count() int {
	ds.RLock()
	defer ds.RUnlock()
	return len(ds.cache)
}

Go 不支持递归互斥锁。而且有观点认为根本不应该使用它们:

stackoverflow.com

递归锁定在 Go 中的实现

标签:go

但如果你确实需要它们,可以使用专门的库,例如这个:

GitHub GitHub

90TechSAS/go-recursive-mutex

recmutex 是一个用于处理递归互斥锁的微型互斥锁库 - 90TechSAS/go-recursive-mutex

我同意递归的读写锁通常会导致问题。现在我已经退休了,正在实现一个干净版本的UniVerse,这是一个Pick模型数据库。在某种连接形式中,Pick TFile和Prime Information的TRANS()结构可以从主键读取表行(和列)。由于行存储在哈希表中,哈希桶上持有读写锁。因此,您需要嵌套读写锁,以便在次要读取终止时仍保持锁定。我的解决方案是跟踪请求(按表和哈希桶),并在计数降至零时释放读写锁。

我当前的问题是每个用户都是一个独立的goroutine。如果某些用户代码创建了无限循环,如何终止该goroutine,或使其跳出"某个循环"。如果它正在"循环",它不会通过select循环来查看上下文取消。

en.wikipedia.org

可重入互斥锁

在计算机科学中,可重入互斥锁(递归互斥锁、递归锁)是一种特殊类型的互斥设备,可以被同一进程/线程多次锁定而不会导致死锁。

当互斥锁已被锁定时,对普通互斥锁执行"锁定"操作的任何尝试都会失败或阻塞;而在递归互斥锁上,当且仅当加锁线程是已经持有该锁的线程时,此操作才会成功。通常,递归…

也许这能帮上忙 🙂

对我来说,这看起来像是数据库事务。也许我理解错了

在Go语言中,sync.Mutex 是不可重入锁(non-reentrant mutex)。这意味着同一个goroutine不能多次获取同一个互斥锁而不释放它。在你的代码中,get 方法获取锁后,在锁仍然持有的情况下调用了 count 方法,而 count 方法也尝试获取同一个锁,这就导致了死锁。

具体机制如下:

  1. get 方法调用 ds.Lock() 获取锁
  2. 在锁仍然持有的情况下,get 方法调用 count 方法
  3. count 方法尝试调用 ds.Lock() 再次获取同一个锁
  4. 由于锁已经被当前goroutine持有,count 方法会一直阻塞等待锁释放
  5. get 方法只有在 count 方法返回后才能释放锁
  6. 这就形成了循环等待,导致死锁

示例代码展示问题所在:

package main

import (
	"fmt"
	"sync"
)

type DataStore struct {
	sync.Mutex
	cache map[string]string
}

func New() *DataStore {
	return &DataStore{
		cache: make(map[string]string),
	}
}

func (ds *DataStore) set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.cache[key] = value
}

// 有问题的get方法 - 会导致死锁
func (ds *DataStore) get(key string) string {
	ds.Lock()
	defer ds.Unlock()
	
	// 问题:在锁持有的情况下调用另一个需要锁的方法
	if ds.count() > 0 { // 这里会死锁
		item := ds.cache[key]
		return item
	}
	return ""
}

func (ds *DataStore) count() int {
	ds.Lock()         // 这里会阻塞,因为锁已经被get方法持有
	defer ds.Unlock()
	return len(ds.cache)
}

// 修复后的get方法
func (ds *DataStore) getFixed(key string) string {
	ds.Lock()
	defer ds.Unlock()
	
	// 直接访问缓存,不调用需要锁的方法
	if len(ds.cache) > 0 {
		item := ds.cache[key]
		return item
	}
	return ""
}

// 或者将count方法改为不需要锁的版本
func (ds *DataStore) countFixed() int {
	// 不获取锁,由调用方保证线程安全
	return len(ds.cache)
}

func main() {
	store := New()
	store.set("Go", "Lang")
	
	// 使用修复后的方法
	result := store.getFixed("Go")
	fmt.Println(result)
	
	// 或者这样使用count方法(在锁保护下)
	store.Lock()
	count := store.countFixed()
	store.Unlock()
	fmt.Println("Count:", count)
}

修复方案:

  1. 在持有锁的方法中不要调用其他需要相同锁的方法
  2. 将不需要锁保护的逻辑提取到独立的方法中
  3. 使用 sync.RWMutex 来区分读锁和写锁,允许多个读操作并发执行

使用 sync.RWMutex 的改进版本:

package main

import (
	"fmt"
	"sync"
)

type DataStore struct {
	sync.RWMutex
	cache map[string]string
}

func New() *DataStore {
	return &DataStore{
		cache: make(map[string]string),
	}
}

func (ds *DataStore) set(key string, value string) {
	ds.Lock()
	defer ds.Unlock()
	ds.cache[key] = value
}

func (ds *DataStore) get(key string) string {
	ds.RLock()
	defer ds.RUnlock()
	
	if ds.count() > 0 {
		item := ds.cache[key]
		return item
	}
	return ""
}

func (ds *DataStore) count() int {
	ds.RLock()  // 使用读锁,可以多个goroutine同时获取
	defer ds.RUnlock()
	return len(ds.cache)
}

func main() {
	store := New()
	store.set("Go", "Lang")
	result := store.get("Go")
	fmt.Println(result)
}
回到顶部