go 同步:更清晰地说明文档 sync.RWMutex不支持锁升级/降级

r3i60tvu  于 3个月前  发布在  Go
关注(0)|答案(6)|浏览(42)

你使用的Go版本是什么(go version)?

$ go version
go version go1.14.2 linux/amd64

你做了什么?

尝试找出在sync.RWMutex中升级和降级读写锁的正确语义。

你想看到什么?

sync.Mutex不同,在使用sync.RWMutex时,特别是在升级和降级读写锁方面有些棘手。#4026文档说明了如何执行这些操作,但这本身并没有链接到文档中。
我建议提供一个示例来演示升级和降级锁的操作。也许像这样?

package sync_test

import (
	"fmt"
	"sync"
)

type Map struct {
	mu   sync.RWMutex
	data map[interface{}]interface{}
}

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	v, ok := m.data[key]
	return v, ok
}

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	loaded = true

	// Retry until actual can be read from m.data so the first LoadOrStore wins:
	for ok := false; !ok; {
		if actual, ok = m.data[key]; ok {
			break
		}

		loaded = false

		// Upgrade the read lock to a write lock:
		m.mu.RUnlock()
		m.mu.Lock()

		if m.data == nil {
			m.data = make(map[interface{}]interface{})
		}
		m.data[key] = value

		// Downgrade the write lock to a read lock:
		m.mu.Unlock()
		m.mu.RLock()
	}

	return actual, loaded
}

func ExampleRWMutex() {
	c := Map{}

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		for {
			if v, ok := c.Load("greet"); ok {
				fmt.Println("Load:", v)
				break
			}
		}
		wg.Done()
	}()

	v, loaded := c.LoadOrStore("greet", "hello")
	fmt.Println("LoadOrStore:", v, loaded)

	wg.Wait()

	// Output:
	// LoadOrStore: hello false
	// Load: hello
}
50pmv0ei

50pmv0ei1#

谢谢,但我不赞成这个观点,因为我要说的是 RWMutex 不支持升级或降级锁。正如你的例子所示,唯一的机制是解锁互斥锁,然后再次锁定它。但是将读锁升级为写锁的正常定义是,升级锁是一个原子操作:如果你升级了锁,没有其他goroutine可以获得写锁。解锁 RWMutex 将允许另一个goroutine在 RUnlockLock 之间获取写锁,所以它不属于我所描述的升级操作。

gc0ot86w

gc0ot86w2#

@ianlancetaylor 通常情况下,我认为Go标准库文档对于正确使用它的方式非常有指导意义,所以我惊讶于RWMutex没有关于升级和降级锁的良好文档。
确实没有原子性的升级或降级,但这一点根本没有提到。至少我认为在文档注解中提及这一点是有用的。我请求提供一个示例,以便在尽管不是原子性的情况下,也能指导如何使用RWMutex
事实上,在我的例子中有一个微妙的错误,其中两个LoadOrStore调用可能会竞争返回它们自己的value参数,显示了这个互斥锁是多么难以使用:

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	loaded = true

	// Retry until actual can be read from m.data so the first LoadOrStore wins:
	for ok := false; !ok; {
		if actual, ok = m.data[key]; ok {
			break
		}

		// Upgrade the read lock to a write lock:
		m.mu.RUnlock()
		m.mu.Lock()

		if m.data == nil {
			m.data = make(map[interface{}]interface{})
		}

		// Upgrading the lock is not atomic, so another writer may have raced us:
		if _, ok := m.data[key]; !ok {
			m.data[key] = value		
			loaded = false
		}

		// Downgrade the write lock to a read lock:
		m.mu.Unlock()
		m.mu.RLock()
	}

	return actual, loaded
}
izkcnapc

izkcnapc3#

你对这个进行了基准测试吗?它比单个Map加互斥锁或条纹互斥锁更快吗?

pgccezyw

pgccezyw4#

对不起,如果我没有说清楚:确切的例子并不重要。我们可以用任何例子来替换那些保护了很多读者和一个写作者的内容。
这个功能请求是为了添加一个关于如何在sync.RWMutex上安全地升级和降级写锁的示例,因为没有原子性的方法来实现这一点。这个示例的关键在于明确显示在写入之前和之后需要重新读取的需求。

tvmytwxo

tvmytwxo5#

我建议在RWMutex文档中简单说明它不支持升级或降级锁。现在的文本似乎试图表达这一点,但实际上很难理解:
如果一个goroutine持有一个读写互斥锁(RLock)进行读取,而另一个goroutine可能调用Lock,那么没有goroutine应该期望能够获取读锁,直到最初的读锁被释放。特别是,这禁止递归读锁定。这是为了确保锁最终可用;阻塞的Lock调用将排除新的读者获取锁。
为什么不这样写呢:
如果一个goroutine持有一个读写互斥锁(RLock)或Lock,那么在不先释放当前持有的情况下,它不能再次获取这两个锁。这也禁止了递归锁定以及将RLock升级为Lock或将Lock降级为RLock。
然后我们可以提供一个示例,说明由于升级是不可能的,所以在调用RUnlock和Lock之间考虑其他goroutine可能获得锁是很重要的。这基本上意味着如果你正在尝试执行CAS风格的原子操作,那么你必须在Lock之后重复比较,因为状态可能在RUnlock和Lock之间发生了很大的变化。

ovfsdjhp

ovfsdjhp6#

我完全同意,文档中应该更清楚地说明这一点。目前仍然不够明确。
作为一名系统程序员,我的期望是,读写锁的锁定是通过队列实现的,这样等待的写入者(或读取者)就不会永远被饿死。但考虑到这种实现方式,似乎并不能保证这一点。这是真的吗?

相关问题