【问题标题】:Use a single mutex across multiple goroutines跨多个 goroutine 使用单个互斥锁
【发布时间】:2020-11-07 18:36:28
【问题描述】:

我正在尝试减少我的 discord 机器人发出的 http 请求数量。

它正在从 API 中读取数据。

使用获取的数据更新内部数据库并输出更改。

问题是:机器人所在的每个服务器的数据库都不同,这就是我使用 go 例程的地方。但是,有些服务器需要获取相同的数据,这里是我想减少 http 请求的地方。现在我正在发出请求,无论我是否已经获取了一个字符。我想创建一些可以在 go 例程之间共享的数据,然后在这些数据中进行请求搜索。

有人建议我使用互斥锁。我想。原题:Working with unbuffered channels in golang

我制作了我尝试过的真实代码的骨架:https://play.golang.org/p/mt229ns1R8m

在此示例中,master := make([][]map[string]interface{}, 0) 正在模拟不和谐服务器。 CharsChars2 将是每个单独服务器的跟踪字符。 char "Test" 对它们都是相互的,所以它应该只从 API 中获取一次。

它正在输出这个:

[[map[Level:15 Name:Test] map[Level:150 Name:Test2]] [map[Level:1500 Name:Test3] map[Level:15 Name:Test]]]
------
A call would be made
A call would be made
A call would be made
A call would be made
Cache: [map[Level:150 Name:Test2] map[Level:15 Name:Test]]Cache: [map[Level:15 Name:Test] map[Level:1500 Name:Test3]]Done

我期待的输出是:

[[map[Level:15 Name:Test] map[Level:150 Name:Test2]] [map[Level:1500 Name:Test3] map[Level:15 Name:Test]]]
------
A call would be made
A call would be made
A call would be made
Cache: [map[Level:150 Name:Test2] map[Level:15 Name:Test] map[Level:1500 Name:Test3]]Done

但是每个 goroutine 都会生成一个新的缓存。我怎样才能解决这个问题? 谢谢。

【问题讨论】:

  • 看起来Name 必须是您的缓存键。为什么要保留一组地图?
  • @BurakSerdar map[string]interface{} 是一个字符。不和谐服务器可以跟踪多个字符,因此[]map[string]interface{}。由于该机器人存在于许多服务器中,因此它最终成为[][]map[string]interface{}。 Name 是唯一不会改变的 char 信息的值,这就是为什么我使用它来比较本地数据和获取的数据的原因。如果有什么变化(级别、职业、成就等),我会比较所有其他字段。
  • 如果你只通过字符名缓存而不关心哪个服务器,使用map[string]map[string]interface{},这样你可以通过检查m[name]来检查你是否缓存了字符。
  • @BurakSerdar 我试过这个:play.golang.org/p/ChFMz13QJkW 仍然得到 2 个缓存而不是一个
  • 您正在为每个服务器创建一个新的缓存。将缓存创建移到 goroutine 创建之外。

标签: go concurrency synchronization mutex


【解决方案1】:

这里有太多未知数,我无法真正写出合适的设计,但让我们做一些笔记:

  • 如果可能的话,尽量不要使用interface{}。在这种情况下,似乎它一定是可能的,虽然我不确定实际的类型是什么。

  • 尽量让您的数据简单,但不要简单。在这种情况下,可能意味着:有一个数据结构用于“与 Discord 服务器通信的事物”和一个单独的数据结构用于“与本地数据库通信的事物”(这是缓存数据库吗?如果是这样,使缓存条目无效的标准是什么?)。但是,如果一个“字符”(无论是什么——显然是一个字符串)可以在每个 Discord 服务器中具有不同的属性,这意味着您在本地数据库中的索引不仅仅是一个字符,而是一个 pair values:字符串值本身加上一个 Discord-server-identifier。

这可能会给你一个这样的功能界面:

var cacheServer *CacheServer

func InitCacheServer() error {
    cacheServer = ... // whatever it takes to initialize the cache server
}

(我假设缓存服务器的延迟初始化。如果您可以进行预先初始化,则可以放弃下面的下一个测试。将ValueType 替换为名称的缓存查找结果的类型。 )

func (DiscordServer ds) Get(name string) (ValueType, error) {
    if cacheserver == nil {
        if err := InitCacheServer(); err != nil {
            return nil, err
        }
    }
    // Do a cache lookup.  Tell the cache server that if there
    // is no entry, it should return a NoEntry error and we will
    // fill the cache ourselves, so it should hold this slot as
    // "will be filled, so wait for it".
    slot, v, err := cacheServer.Lookup(name, ds.identity, CacheServer.IntentToFill)

    if err == CacheServer.NoEntry {
        // We have the slot held.  Try to look up the right info
        // directly in the Discord server, then cache it.
        v, err = ds.UncachedGet(name)
        // Tell cache server that this is the value, or that it should
        // produce this error instead of NoCache.
        cacheServer.FillSlot(slot, v, err)
    }
}

您可能只想缓存 一些 错误类型,而不是全部;这是另一个需要我无法在此处提供的答案的设计问题。还有其他方法可以做到这一点,也不一定需要slot 指针返回值;我刚刚为这个例子选择了这个。

请注意,大部分“艰苦的工作”现在都在缓存服务器中,这肯定需要一些花哨的步法。特别是你会想要锁定整个数据结构一段时间,用它来找到正确的槽,然后保持槽本身,以便槽的其他用户必须等待,同时释放整体锁,以便 other 条目无需等待。这引入了锁定顺序约束:小心避免死锁。一种可行的方法是:

type CacheServer struct {
    lock sync.Mutex
    data map[string]map[string]*Entry
    // more fields
}

type Entry {
    lock        sync.Mutex
    cachedValue ValueType
    cachedError error
}

(您将需要更多类型,例如Intent——现在只是两个枚举整数——下面,可能还有更多字段;​​这只是一个骨架。)

func (cs *CacheServer) Lookup(name, srv string, flags Intent) (*Entry, ValueType, error) {
    cs.lock.Lock()
    defer cs.lock.Unlock()
    // first, look up the server - if it does not exist, create one
    smap := cs.data[srv]
    if smap == nil {
        cs.data[server] = make(map[string]*Entry)
    }
    entry := smap[name]
    if entry == nil {
        // no cached entry - if this is a pure lookup, just error,
        // but if not, make a locked entry
        if flags == CacheServer.IntentToFill {
            // make a new entry and return with it locked
            entry = &Entry{}
            smap[name] = entry
            entry.lock.Lock() // and do not unlock
        }
        return entry, nil, NoEntry
    }
    entry.lock.Lock() // wait for someone to fill it, if needed
    defer entry.lock.Unlock()
    return nil, entry.cachedValue, entry.cachedError
}

您还需要一个例程来填充和释放条目,但这很简单。如果您愿意,您可以在Entry 类型上而不是CacheServer 类型上将此方法设置为,至少在这个特定的原型中,不需要直接使用缓存服务器数据结构。不过,如果您开始对缓存失效有更多兴趣,那么访问CacheServer 对象可能会很好。

注意:我已经设计了这个,以便您可以在没有意图填充的情况下进行缓存查找,如果这有用的话。如果不是,则没有理由为 Intent 参数而烦恼。

【讨论】:

  • 感谢您的精彩回复。所以,设计如下:这个字符轨道是机器人的插件。如果服务器所有者激活它,那么它会创建一个数据库。该数据库中的一个字段是[]Char,它是正确字符结构的一部分。我之前发送的代码只是一个骨架代码,它模仿了我对真实代码所做的事情。字符错误、数据库错误,所有这些都已在代码中处理。代码正在运行,我的最终目标只是减少 http 请求的数量。 (继续...)
  • 因为它是从游戏 API 中读取的,所以它是不断变化的。我每 10 分钟从 API 获取一次。当我得到这个新的传入数据时,我会将它与我在内部存储的数据进行比较。如果有任何变化,我会更新数据库并将消息发送到相应的不和谐服务器。我需要做的是:例如,如果服务器 A 获取 char XYZ 的数据,我为什么要再次获取相同 char 的数据到服务器 B?我需要一个缓存,就在 go 例程工作时,并且在实际发出 http 请求之前,检查指定的字符是否已经被获取。 (继续...)
  • 这样,我可以大大减少http请求的数量。例如,我有一个 char 在 12 个不同的服务器中被跟踪。这意味着我每 10 分钟获取该字符 12 次。完全是浪费。我应该只能每 10 分钟获取一次,并将获取的数据用于我的所有服务器。我在想我可能会根据您的回复更改机器人设计,并可能制作一个“主”数据库,其中包含在每台服务器上跟踪的所有字符,并稍后将新获取的数据单独与不和谐服务器进行比较。会考虑的。再次感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-07-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-19
  • 2017-07-31
相关资源
最近更新 更多