缓存是应用程序中常用的解决方案,以避免重复昂贵的计算,而倾向于一些可以随时在内存中获取的值。一个简单的缓存策略是将缓存作为数据库读取访问之上的一个薄层,如下所示:
package main
import "sync"
type Database struct {
cache map[string][]byte
lock sync.RWMutex
}
func (db *Database) GetItem(key []byte) ([]byte, error) {
db.lock.RLock()
if value, ok := db.cache[string(key)]; ok {
db.lock.RUnlock()
return value
}
db.lock.RUnlock()
return db.readFromDatabase(key)
}
func (db *Database) WriteItem(key, value []byte) error {
if err := db.writeToDatabase(key, value); err != nil {
return err
}
db.lock.Lock()
db.cache[string(key)] = value
db.lock.Unlock()
return nil
}
这种策略对于那些你有重复读取访问某个值的请求的应用来说非常有效,可以防止你执行潜在的昂贵的数据库查询,并利用内存中的快速访问。缓存是非常有帮助的。然而,对于某些问题来说,一个缓存肯定是不够的。
忙碌的工人问题
想象一下,你有数以千计或更多的进程试图在同一时间进行同样昂贵的计算。也许所有的进程都被通知需要计算某些需要很长时间的数字,或者他们需要执行一个极其昂贵的操作,如果过度的话,会使你的CPU或内存达到最大。在我的项目Prysm中,这是一个相当普遍的问题,它有许多不同的工作者,以goroutine的形式,经常试图执行重复的工作。一个天真的解决方案是简单地利用缓存策略来避免重复计算,如上所示。但是,如果缓存中还没有你关心的值,而成千上万的工作者已经在尝试进行昂贵的计算,那该怎么办?也许有许多工作者正试图执行一个已经在进行中的动作。这就是我们所说的进展中的缓存的一个很好的用例。让我们看一个例子:
package main
import "sync"
func main() {
var wg sync.WaitGroup
numWorkers := 1000
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func(w *sync.WaitGroup) {
defer wg.Done()
doSomethingExpensive()
}(&wg)
}
wg.Wait()
}
func doSomethingExpensive() {
// Get result from cache if it has already completed.
value, ok := checkCache()
ok{
// Do something with the cached value.
}
// Expensive operation which can take a few seconds to complete...
}
但是,如果当所有1000个工作者都在尝试执行这个昂贵的操作时,缓存中还没有任何东西,怎么办?那么,所有的人都会开始执行你的昂贵的操作,你的计算机可能会爆炸,而我们的缓存几乎没有用。相反,我们可以利用Go通道的力量,将工作标记为进行中,而让所有工作者共享哪个工作者先完成的相同返回值。让我们考虑一下如何做到这一点。
首先,如果我们关心的请求已经在进行中,我们需要一种方法来阻止一个工作者执行昂贵的计算。其次,一旦一个工作者完成了昂贵的计算,我们需要立即通知所有试图进行相同计算的工作者的返回值。为了完成第一个任务,我们可以利用共享地图的组合来检查一个请求是否正在进行中,然后通过初始化一个通道并将其附加到一些共享列表中的请求来订阅正在进行的请求。最后,一旦一个工作者完成了计算,它就可以将结果发送给订阅了该进行中请求的所有接收者。让我们来看看它的操作:
type service struct {
inProgress map[string]bool
awaitingCompletion map[string][]chan string
lock sync.RWMutex
}
上面,我们定义了一个简单的结构,用于封装这些信息。在我们的例子中,我们昂贵的计算结果是一些字符串值,请求身份也是一个字符串。我们对请求身份的两个映射进行跟踪:第一个被称为inProgress ,它将被工作者用来检查昂贵的计算是否已经在进行。第二个被称为awaitingCompletion ,它是一个等待被通知正在进行的请求的通道的列表。它们本质上是其他正在订阅当前正在进行的工作者的计算值的工作者。我们使用一个mutex来使这些地图成为线程安全的。
接下来,我们开始我们的main 函数,模拟5个工作者同时进行一些昂贵的操作:
func main() {
ss := &service{
inProgress: make(map[string]bool),
awaitingCompletion: make(map[string][]chan string),
}
// Create N = 5 workers.
numWorkers := 5
var wg sync.WaitGroup
wg.Add(numWorkers)
// Launch N goroutines performing the same work:
// a request with ID "expensivecomputation".
requestID := "expensivecomputation"
for i := 0; i < numWorkers; i++ {
go func(w *sync.WaitGroup, id string) {
defer wg.Done()
ss.doWork(id)
}(&wg, requestID)
}
// Wait for all goroutines to complete work.
wg.Wait()
fmt.Println("Done")
}
接下来,我们看一下关键函数:doWork(requestID string) 。我们先用Go的伪代码把它写出来:
package main
import "time"
func (s *service) doWork(requestID string) {
if ok := s.inProgress[requestID]; ok {
// Subscribe to be notified of when the in-progress
// request completes via a channel.
// Await the response from the worker currently in-progress...
return
}
// Mark the requestID as in progress.
s.lock.Lock()
s.inProgress[requestID] = true
s.lock.Unlock()
// Perform some expensive, lengthy work (time.Sleep used to simulate it).
time.Sleep(time.Second * 4)
response := "the answer is 42"
// Send to all subscribers.
s.lock.RLock()
receiversWaiting, ok := s.awaitingCompletion[requestID]
s.lock.RUnlock()
if ok {
for _, ch := range receiversWaiting {
ch <- response
}
}
// Reset the in-progress data for the requestID.
s.lock.Lock()
s.inProgress[requestID] = false
s.awaitingCompletion[requestID] = make([]chan string, 0)
s.lock.Unlock()
}
我们在地图访问周围加锁,以减少实际应用中的锁争夺。接下来,我们填写if ok := inProgress[key]; ok 的逻辑:
if ok := s.inProgress[requestID]; ok {
// We add a buffer of 1 to prevent blocking
// the sender's goroutine.
responseChan := make(chan string, 1)
defer close(responseChan)
lock.Lock()
s.awaitingCompletion[requestID] = append(s.awaitingCompletion[requestID], responseChan)
lock.Unlock()
fmt.Println("Awaiting work in-progress")
val := <-responseChan
fmt.Printf("Work result received with value %s\n", val)
return
}
把它放在一起,我们得到:
package main
import (
"fmt"
"sync"
"time"
)
type service struct {
inProgress map[string]bool
awaitingCompletion map[string][]chan string
lock sync.RWMutex
}
func (s *service) doWork(requestID string) {
s.lock.RLock()
if ok := s.inProgress[requestID]; ok {
s.lock.RUnlock()
responseChan := make(chan string, 1)
defer close(responseChan)
s.lock.Lock()
s.awaitingCompletion[requestID] = append(s.awaitingCompletion[requestID], responseChan)
s.lock.Unlock()
fmt.Println("Awaiting work completed")
val := <-responseChan
fmt.Printf("Work result received with value %s\n", val)
return
}
s.lock.RUnlock()
s.lock.Lock()
s.inProgress[requestID] = true
s.lock.Unlock()
// Do expensive operation
fmt.Println("Doing expensive work...")
time.Sleep(time.Second * 4)
result := "the answer is 42"
s.lock.RLock()
receiversWaiting, ok := s.awaitingCompletion[requestID]
s.lock.RUnlock()
if ok {
for _, ch := range receiversWaiting {
ch <- result
}
fmt.Println("Sent result to all subscribers")
}
s.lock.Lock()
s.inProgress[requestID] = false
s.awaitingCompletion[requestID] = make([]chan string, 0)
s.lock.Unlock()
}
func main() {
ss := &service{
inProgress: make(map[string]bool),
awaitingCompletion: make(map[string][]chan string),
}
numWorkers := 5
var wg sync.WaitGroup
wg.Add(numWorkers)
requestID := "expensivecomputation"
for i := 0; i < numWorkers; i++ {
go func(w *sync.WaitGroup, id string) {
defer wg.Done()
ss.doWork(id)
}(&wg, requestID)
}
wg.Wait()
fmt.Println("Done")
}
现在运行main.go文件。go run main.go,我们观察到它如期发生:
Doing expensive work...
Awaiting work completed
Awaiting work completed
Awaiting work completed
Awaiting work completed
Sent result to all subscribers
Work result received with value the answer is 42
Work result received with value the answer is 42
Work result received with value the answer is 42
Work result received with value the answer is 42
Done
5个工作者中的一个正在做昂贵的工作,其余的正在等待它的完成。一旦它在4秒后完成,4个订阅的工作者就会正确地收到 "答案是42 "的值希望这个简单的方法可以帮助你,当你想减少后台例程执行的重复工作时,利用Go通道的力量来阻止goroutine,直到收到一个值。
注意:上面的代码不适合用于生产,因为在生产中,你需要有更好的方法来处理goroutine上下文的取消,以及用更聪明的方法来命名请求和订阅者,而不是仅仅使用天真的映射。