Read preferring RW mutex lock in Golang

huangapple go评论71阅读模式
英文:

Read preferring RW mutex lock in Golang

问题

我需要一个在golang中偏向读取的互斥锁。是否有一个golang的包可以满足我的需求?我尝试过sync.RWMutex,但它似乎是偏向写入的锁。以下是我尝试区分Go的RWMutex的代码:

package main

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

func main() {

	y := &resource{x: 10}

	go func() {
		defer fmt.Println("done first read")
		y.RLock()
		defer y.RUnlock()
		go func() {
			defer fmt.Println("done first write")
			fmt.Println("first write req")
			y.Lock()
			fmt.Println("after first write granted")
			defer y.Unlock()
		}()
		time.Sleep(time.Second)
		go func() {
			defer fmt.Println("done second read")
			fmt.Println("second read req")
			y.RLock()
			fmt.Println("after second read granted")
			defer y.RUnlock()
		}()

		time.Sleep(10 * time.Second)
	}()

	time.Sleep(time.Minute)

}

type resource struct {
	sync.RWMutex
	x int
}

输出:

first write req
second read req
done first read
after first write granted
done first write
after second read granted
done second read

第二个读取器一直等待,直到写入器释放锁。

英文:

I need a read preferring RW mutex in golang. Is there a package in golang that will satisfy my needs. I tried sync.RWMutex, but it seems to be write preferring lock. Here goes my attempt to distinguish Go's RWMutex,

package main

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

func main() {

y := &resource{x: 10}

go func() {
	defer fmt.Println("done first read")
	y.RLock()
	defer y.RUnlock()
	go func() {
		defer fmt.Println("done first write")
		fmt.Println("first write req")
		y.Lock()
		fmt.Println("after first write granted")
		defer y.Unlock()
	}()
	time.Sleep(time.Second)
	go func() {
		defer fmt.Println("done second read")
		fmt.Println("second read req")
		y.RLock()
		fmt.Println("after second read granted")
		defer y.RUnlock()
	}()

	time.Sleep(10 * time.Second)
}()

time.Sleep(time.Minute)

}

type resource struct {
	sync.RWMutex
	x int
}

Output:

first write req
second read req
done first read
after first write granted
done first write
after second read granted
done second read

Second reader is kept waiting till the writer releases the lock.

答案1

得分: 9

sync.RWMutex实现了写优先和读优先的锁定。这取决于你如何使用它来实现写优先或读优先。

以你提供的维基百科链接中的伪代码作为读优先情况下的锁定(Lock-For-Read)的例子:

  • 输入:互斥锁 m,条件变量 c,整数 r(等待读取的读者数),标志 w(等待写入的写者)。
  • 锁定 m(阻塞)。
  • 当 w 为真时:
  • 等待 c,m[a]。
  • 增加 r。
  • 解锁 m。

在读优先情况下,如果你按照上述读锁定模式进行操作,那么写锁定模式(Lock-For-Write)如下:

  • 锁定 m(阻塞)。
  • 当 w 或 r > 0 时:
  • 等待 c,m。
  • 将 w 设置为真。
  • 解锁 m。

你可以在RWMutex的实现中看到这种机制的运作方式。记住,Go框架只是Go代码 - 查看代码以了解其实现方式:

https://golang.org/src/sync/rwmutex.go?s=879:905#L20

代码中需要注意的一点是上述代码中的rw.readerSem,它给出了维基百科示例模式中的整数 r,在Go等编程语言中被称为信号量(Semaphore):

http://www.golangpatterns.info/concurrency/semaphores

在代码中,真正的等待过程发生在第37行的runtime_Semacquire()函数中:

https://golang.org/src/sync/runtime.go

了解了这一点,并看到RWMutex.RLock()如何增加读取计数,你可以相应地重构你的代码。

再看看RWMutex.RUnlock如何减少读取计数,但更重要的是看看RWMutex.Lock()如何强制等待所有活动读者:

这很可能是你看到第二个读者等待的原因。

请记住,信号量不仅在你创建的RWMutex实例之间共享,而且在整个运行时中用于调度其他goroutine和其他锁定。因此,试图强制实现某种模式可能会对应用程序造成更多的伤害,而不是带来好处。

我的建议是退一步,考虑一下为什么你要在架构中使用读优先的锁定。你的应用程序真的达到了CPU上下文切换减慢高频率应用程序的性能水平吗?我认为可以采取更系统化的方法,而不是仅仅因为听起来很酷,听起来可以解决所有问题而尝试实现“读优先锁定”模式。你的基准测试结果如何?输入数据的大小是多少,有多少并发进程?它必须是共享的吗?它的内存消耗是否在X GB以下,你是否可以切换到将数据放在堆栈上(例如通道,无互斥锁)?读取的数据是否可以放在堆栈上,同时保持写入集合进行锁定?与必须将事物保留在堆上相比,GC多久清理一次堆栈?等等。

英文:

sync.RWMutex implements both write preferred and read preferred locking. It all depends on how you use it to gain either write preferred or read preferred.

Taking your wikipedia link pseudo code as an example of Lock-For-Read (in a read-preferred situation):

* Input: mutex m, condition variable c, integer r (number of readers waiting), flag w (writer waiting).
* Lock m (blocking).
* While w:
* wait c, m[a]
* Increment r.
* Unlock m.

And the Lock-For-Write pattern in a read-preferred sistuation as long as you are following the pattern above for Lock-For-Reads:

* Lock m (blocking).
* While (w or r > 0):
* wait c, m
* Set w to true.
* Unlock m.

You can see this mechanism at play within how the RWMutex is implemented. Remember, the Go framework is just Go code - view the code to see how it is implemented:

https://golang.org/src/sync/rwmutex.go?s=879:905#L20

29	// RLock locks rw for reading.
30	func (rw *RWMutex) RLock() {
31		if race.Enabled {
32			_ = rw.w.state
33			race.Disable()
34		}
35		if atomic.AddInt32(&rw.readerCount, 1) < 0 {
36			// A writer is pending, wait for it.
37			runtime_Semacquire(&rw.readerSem)
38		}
39		if race.Enabled {
40			race.Enable()
41			race.Acquire(unsafe.Pointer(&rw.readerSem))
42		}
43	}

A key to note is the rw.readerSem in the code above which gives you your integer r in the wikipedia example pattern, which languages (like Go and others) call a Semaphore:

http://www.golangpatterns.info/concurrency/semaphores

The real meat of of the wait is on line 37, for the runtime_Semaquire():

https://golang.org/src/sync/runtime.go

11	// Semacquire waits until *s > 0 and then atomically decrements it.
12	// It is intended as a simple sleep primitive for use by the synchronization
13	// library and should not be used directly.
14	func runtime_Semacquire(s *uint32)

Knowing that, and seeing how a RWMutex.RLock() increments a read that number, you can refactor your code accordingly.

Take a look at how RWMutex.RUnlock decrements that but most importantly how RWMutex.Lock() forces the wait for all active readers:

71	// Lock locks rw for writing.
72	// If the lock is already locked for reading or writing,
73	// Lock blocks until the lock is available.
74	// To ensure that the lock eventually becomes available,
75	// a blocked Lock call excludes new readers from acquiring
76	// the lock.
77	func (rw *RWMutex) Lock() {
78		if race.Enabled {
79			_ = rw.w.state
80			race.Disable()
81		}
82		// First, resolve competition with other writers.
83		rw.w.Lock()
84		// Announce to readers there is a pending writer.
85		r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
86		// Wait for active readers.
87		if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
88			runtime_Semacquire(&rw.writerSem)
89		}
90		if race.Enabled {
91			race.Enable()
92			race.Acquire(unsafe.Pointer(&rw.readerSem))
93			race.Acquire(unsafe.Pointer(&rw.writerSem))
94		}
95	}

That's most likely why you are seeing the 2nd reader waiting.

Remember, the Semaphores are shared across not just the RWMutex instance you created, but also throughout the runtime to schedule around other goroutines and other locks. Hence why trying to force a pattern can do more harm than good overall in an app.

My advise would be to take a step back and consider why you want read-preferred locking in your architecture at all. Are you truly at a performance level that CPU context switching is slowing down your high frequency application? I'd say there's a more systematic approach that could be taken instead of trying to implement a 'read-preferred locking` pattern just because it sounds cool and sounds like it solve all your problems. What are your benchmarks numbers? What is the size of input data, and across how many concurrent processes? Does it have to be shared? Is it under X GB of memory consumption and can you switch to putting things on the stack (e.g. channels, no mutex locking)? What about the read data on the stack and keep a write set for locking? For how long until the GC cleans up the stack vs having to keep things on the heap? Etc etc.

答案2

得分: 3

似乎你可以使用sync.WaitGroup来实现所需的行为,例如:

var wg sync.WaitGroup
go func() {
    defer fmt.Println("done second read")
    fmt.Println("second read req")
    y.RLock()   //等待写入
    wg.Add(1)   //报告忙碌
    fmt.Println("after second read granted")
    defer wg.Done() //报告完成
    defer y.RUnlock()
}()

//强制写入等待所有读取器
go func() {
    defer fmt.Println("done first write")
    fmt.Println("first write req")
    wg.Wait()  //等待所有读取器
    y.Lock()
    fmt.Println("after first write granted")
    defer y.Unlock()
}()

你可以尝试使用 https://play.golang.org/p/y831xIrglj 进行测试。

英文:

Seems you can achieve desired behaviour with sync.WaitGroup sync primitive for example

var wg sync.WaitGroup
go func() {
			defer fmt.Println("done second read")
			fmt.Println("second read req")
			y.RLock()   //wait writer
			wg.Add(1)   //report busy
			fmt.Println("after second read granted")
			defer wg.Done() //report done
			defer y.RUnlock()
		}()
//forcing writer to wait all readers
go func() {
			defer fmt.Println("done first write")
			fmt.Println("first write req")
			wg.Wait()  //wait all readers
			y.Lock()
			fmt.Println("after first write granted")
			defer y.Unlock()
		}()

You can try https://play.golang.org/p/y831xIrglj

huangapple
  • 本文由 发表于 2016年4月11日 20:27:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/36548702.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定