DAY 3:Read-Write-Lock Pattern,三人成虎,一人打虎!

什麽是 Read-Write-Lock Pattern?

多读单写。将 lock 分为 read 与 write 两种,让 lock 效能更佳,read 行为不会改变资料,所以 read lock 时可以再同时 read,达到「多读」,而 write 行为会改变资料,所以 write lock 不能再同时 read 与 write 时,达到「单写」

read, write 中在取得 lock 的时候,只要碰到以下两种「冲突 conflict」状态,就会等待此冲突结束,再取得 lock:

  • read 与 write 的 lock 同时存在
  • write 与 write 的 lock 同时存在

而在以下情形,会直接取得 lock,不会等待此状态结束:

  • read 与 read 的 lock 同时存在

问题情境

延续上篇 Single Threaded Execution Pattern 的情境,

当设计一个点赞系统上线,允许一人有多个赞,此时会有多个 Client 进行点赞,系统需确保点赞数量正确无误,还须确保读写效能不受到太大影响。

有可能会发生,A、B、C、D 同时两人点赞与两人读赞,由於 A 在读取赞数後,A 还没写入 B、C 又进行了读取,随後 A 加一写入後,B、C 现有的资料还是未加一的,B、C 将资料拿去显示,就会是旧的数字,如图:

相关的 code 在Github - go-design-patterns

实作有问题的系统如下,A、D 不断地写入,B、C 不断的读取:

package main

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

type Like struct {
	count uint8
}

func (l *Like) Add(writerID string) {
	fmt.Printf("%s change count: %d\n", writerID, l.count+1)
	l.count++
}

func (l *Like) Show(readerID string) {
	fmt.Printf("%s read count: %d\n", readerID, l.count)
}

func AddLikes(writerID string, like *Like) {
	for i := 0; i < 100; i++ {
		like.Add(writerID)
	}
}

func ReadLikes(readerID string, like *Like) {
	for i := 0; i < 200; i++ {
		like.Show(readerID)
	}
}

func main() {
	like := new(Like)
	go AddLikes("A", like)
	go ReadLikes("B", like)
	go ReadLikes("C", like)
	go AddLikes("D", like)
	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

执行後会发现当 A、D 改变 like.count 之後,B、C 并不同步,如图 D 已经将 like.count 改成 27,但 B 其中一次读取是 20,C 其中一次读取也是 20

解决方式

在写入处Add()与读取处Show()中加入sync.Mutex{}的 lock,使读写不同时进行,如图:

package main

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

type Like struct {
	sync.Mutex
	count uint16
}

func (l *Like) Add(writerID string) {
	l.Lock()
	defer l.Unlock()
	fmt.Printf("%s change count: %d\n", writerID, l.count+1)
	l.count++
}

func (l *Like) Show(readerID string) {
	l.Lock()
	defer l.Unlock()
	fmt.Printf("%s read count: %d\n", readerID, l.count)
}

func AddLikes(writerID string, like *Like) {
	for i := 0; i < 100; i++ {
		like.Add(writerID)
	}
}

func ReadLikes(readerID string, like *Like) {
	for i := 0; i < 200; i++ {
		like.Show(readerID)
	}
}

func main() {
	like := new(Like)
	go AddLikes("A", like)
	go ReadLikes("B", like)
	go ReadLikes("C", like)
	go AddLikes("D", like)
	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

但执行之後会发现,B、C 读取害 like.count 被改变的机会降得很低,如图:

这是因为只要 lock 存在,其他 lock 请求就必须等待,因为 B、C 争取 lock 大大降低了整个系统取得 lock 的机会,但我们仔细思考看看,B、C 真的需要竞争此 lock 吗?答案是不用的,因为 B、C 并不会改变 like.count,所以可以同时运行,系统只需注意 A、D 要改变 like.count 的其他人都读写完毕即可,整体运行的时间也会降低,如图:

sync.Mutex{}换成sync.RWMutex{}读写锁,Show()部分的 lock 换成 read-lock,如下:

package main

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

type Like struct {
	sync.RWMutex
	count uint16
}

func (l *Like) Add(writerID string) {
	l.Lock()
	defer l.Unlock()
	fmt.Printf("%s change count: %d\n", writerID, l.count+1)
	l.count++
}

func (l *Like) Show(readerID string) {
	l.RLock()
	defer l.RUnlock()
	fmt.Printf("%s read count: %d\n", readerID, l.count)
}

func AddLikes(writerID string, like *Like) {
	for i := 0; i < 100; i++ {
		like.Add(writerID)
	}
}

func ReadLikes(readerID string, like *Like) {
	for i := 0; i < 200; i++ {
		like.Show(readerID)
	}
}

func main() {
	like := new(Like)
	go AddLikes("A", like)
	go ReadLikes("B", like)
	go ReadLikes("C", like)
	go AddLikes("D", like)
	time.Sleep(10 * time.Second) //等待goroutine执行完毕
}

B、C 多读,A、D 单写,使得写入与读取的机会都变得更有效率,如图:


<<:  Day 1 - 参赛前言

>>:  undefined 、 undeclared 、 null 的区别

Day09 - [丰收款] 安全无虞後,开始建立订单:ATM虚拟帐号篇 (1)

先前花了几天的时间,终於把每次API发送前的安全规定的细碎精工给搞定了,也开了篇幅写了一些关於十六进...

Day02 - 可能发生的费用、目标架构说明

可能发生的费用 云地混合的DevOps环境 AWS CodeCommit AWS CodePipel...

Day 29:Google Map 自订资讯视窗

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...

Day10 滚动式修正与涌浪规划

人生在世不如意,十之八九。唯一能够预测四十年後自己小孩成就的只有那十分之一,叫做一事无成。 这两样东...

@Day30 | C# WixToolset + WPF 帅到不行的安装包 [最终回]

哈哈, 其实拖了很久了! 今天来把最後剩下功能给补齐,修复跟移除, 只是我在看InstallView...