go - 跨多个 goroutine 使用单个互斥锁

标签 go concurrency synchronization mutex

我正在尝试减少我的不和谐机器人发出的 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 都会生成一个新的缓存。我怎样才能解决这个问题?
谢谢。

最佳答案

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

  • 尽量不要使用interface{}完全,如果可能的话。在这种情况下,似乎它必须是可能的,尽管我不确定实际的类型是什么。
  • 尝试使您的数据尽可能简单,但不要更简单。在这种情况下,这可能意味着:有一个数据结构用于“与 Discord 服务器通信的东西”和一个单独的数据结构用于“与本地数据库通信的东西”(这是一个缓存数据库吗?如果是,标准是什么?使缓存条目无效?)。但是,如果一个“字符”(不管是什么——显然是一个字符串)可以在每个 Discord 服务器上具有不同的属性,这意味着您在本地数据库中的索引不仅仅是一个字符,而是一对值:字符串值本身加上不和谐服务器标识符。

  • 这可能会给你一个像这样的功能界面:
    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指针返回值;我刚刚为这个例子选择了这个。
    请注意,大部分“艰苦的工作”现在都在缓存服务器中,这肯定需要一些花哨的步法。特别是你会想要锁定整个数据结构一段时间,用它来找到正确的槽,然后保持槽本身,以便槽的其他用户必须等待,同时释放整体锁,以便其他用户的其他用户条目无需等待。这引入了锁定顺序约束:小心避免死锁。一种应该有效的方法是:
    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 上类型,至少在这个特定的原型(prototype)中,不需要直接使用缓存服务器数据结构。但是,如果您开始对缓存失效有更多兴趣,那么访问 CacheServer 可能会很好。目的。
    注意:我设计了这个,以便您可以在没有意图填充的情况下进行缓存查找,如果这有用的话。如果没有,没有理由打扰 Intent争论。

    关于go - 跨多个 goroutine 使用单个互斥锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62962702/

    相关文章:

    linux - 在 linux 中安装 go 包

    java - 线程需要等待列表更新

    go - google go goroutine 的中断模式(速度问题)

    go - 如何使用 Gorilla Mux 将整个路径与正则表达式匹配

    java - ConcurrentLinkedQueue代码解释

    java - 使用线程更新 JLabel

    python - 在Elasticsearch文档线程中更新数组是否安全?

    java - 多线程 Android 游戏上的图像不稳定

    java - 线程安全与性能

    go - 存储加密/rand 生成的字符串问题