【问题标题】:How to implement Go concurrent map or slice for managing in-use resources faster?如何实现 Go 并发 map 或 slice 以更快地管理使用中的资源?
【发布时间】:2021-12-07 10:04:53
【问题描述】:

您有一个结构,该结构表示一次只有一个用户可以访问的资源。可能看起来像这样:

type Resource struct{
    InUse bool//or int32/int64 is you want to use atomics
    Resource string //parameters that map to the resource, think `/dev/chardeviceXXX`
}

这些资源的数量是有限的,用户会随机同时请求访问它们,因此您将它们打包在管理器中

type ResourceManager struct{
    Resources []*Resource //or a map 
}

我正在尝试找出让经理创建一个函数func (m *ResourceManager)GetUnusedResouce()(*Resouce,error) 的最佳、安全方法,该函数将:

  • 遍历所有资源,直到找到未使用的资源
  • 将其标记为 InUse 并将 *Resouce 返回到调用上下文/goroutine
  • 我会锁定以避免任何系统级锁定 (flock) 并在 Go 中完成这一切
  • 还需要一个函数来标记资源不再使用

现在我在管理器中使用互斥锁来锁定访问,因为我遍历整个切片。它是安全的,但我希望能够通过同时搜索已使用的资源并处理两个尝试将同一资源标记为 InUse 的 goroutine 来加快速度。

更新

我特别想知道是否将资源InUse 字段设置为int64,然后使用atomic.CompareAndSwapInt64 将允许资源管理器在发现未使用的资源时正确锁定:

func (m *ResourceManager)GetUnusedResouce()(*Resouce,error){
    for i := range Resources{
        if atomic.CompareAndSwapInt64(&Resouces[i].InUse,1){
            return Resouces[i],nil
        }
    }
    return nil, errors.New("all resouces in use")
}

任何可以更好地测试这一点的单元测试也将不胜感激。

【问题讨论】:

  • 您可能希望将正在使用的资源(可能由将用于释放的句柄标识)和空闲资源分开存储。
  • 如果对管理器和资源的唯一访问是这里描述的,那么在管理器中保留一个受互斥锁保护的可用资源单链表。 GetUnusedResource 从列表中获取第一个资源。 ReleaseResource 将资源添加到列表的开头。
  • @CeriseLimón 这绝对是安全的做法,但对 Go 并发模型有更深入了解的人可能能够锁定 !InUse 检测,使用类似 atomic.CompareAndSwapInt
  • 我建议采用@cerise-limón 的建议:如果您需要其他人对原子操作是否有效的见解,您不需要原子操作 这一次: 作为一个程序员,你最关心的应该是让你的代码正确;一旦你对相关概念有了扎实的知识,你就可以让它更快(并且仍然正确)。
  • @kostix 问题已更新,以反映我希望看到更多技术和更快的选择,因为我已经有了一个安全的选择

标签: go concurrency


【解决方案1】:

问题中的GetUnusedResouce 函数可能会对所有资源执行比较和交换操作。根据资源数量和应用程序访问模式,执行受互斥体保护的少量操作会更快。

使用单链表实现快速get和put操作。

type Resource struct {
    next     *Resource
    Resource string
}

type ResourceManager struct {
    free *Resource
    mu   sync.Mutex
}

// Get gets a free resource from the manager or returns
// nil when the manager is empty.
func (m *ResourceManager) Get() *Resource {
    m.mu.Lock()
    defer m.mu.Unlock()
    result := m.free
    if m.free != nil {
        m.free = m.free.next
    }
    return result
}

// Put returns a resource to the pool.
func (m *ResourceManager) Put(r *Resource) {
    m.mu.Lock()
    defer m.mu.Unlock()
    r.next = m.free
    m.free = r
}

这是一个在测试中使用的示例:

func TestResourceManager(t *testing.T) {

    // Add free resources to a manager.
    var m ResourceManager
    m.Put(&Resource{Resource: "/dev/a"})
    m.Put(&Resource{Resource: "/dev/b"})

    // Test that we can get all resources from the pool.

    ra := m.Get()
    rb := m.Get()
    if ra.Resource > rb.Resource {
        // Sort ra, rb to make test independent of order.
        ra, rb = rb, ra
    }
    if ra == nil || ra.Resource != "/dev/a" {
        t.Errorf("ra is %v, want /dev/a", ra)
    }
    if rb == nil || rb.Resource != "/dev/b" {
        t.Errorf("rb is %v, want /dev/b", rb)
    }

    // Check for empty pool.

    r := m.Get()
    if r != nil {
        t.Errorf("r is %v, want nil", r)
    }

    // Return one resource and try again.

    m.Put(ra)
    ra = m.Get()
    if ra == nil || ra.Resource != "/dev/a" {
        t.Errorf("ra is %v, want /dev/a", ra)
    }
    r = m.Get()
    if r != nil {
        t.Errorf("r is %v, want nil", r)
    }

}

Run the test on the playground.

如果资源数量存在已知的合理限制,请使用频道。这种方法利用了运行时高度优化的通道实现。

type Resource struct {
    Resource string
}

type ResourceManager struct {
    free chan *Resource
}

// Get gets a free resource from the manager or returns
// nil when the manager is empty.
func (m *ResourceManager) Get() *Resource {
    select {
    case r := <-m.free:
        return r
    default:
        return nil
    }
}

// Put returns a resource to the pool.
func (m *ResourceManager) Put(r *Resource) {
    m.free <- r
}

// NewResourceManager returns a manager that can hold up to
// n free resources.
func NewResourceManager(n int) *ResourceManager {
    return &ResourceManager{free: make(chan *Resource, n)}
}

使用上述TestResourceManager 函数测试此实现,但将var m ResourceManager 替换为m := NewResourceManager(4)

Run the test on the Go playground.

【讨论】:

  • 代码的可读性也很强。我会抽出时间对所有这些进行基准测试。
【解决方案2】:

给定资源是否正在使用不是Resource 本身的属性,而是ResourceManager 的属性。

事实上,没有必要跟踪正在使用的资源(除非您出于问题中未提及的某些原因需要)。正在使用的资源在释放时可以简单地放回池中。

这是一个使用通道的可能实现。不需要一个互斥体,也不需要任何原子 CAS。

package main

import (
    fmt "fmt"
    "time"
)

type Resource struct {
    Data string
}

type ResourceManager struct {
    resources []*Resource
    closeCh   chan struct{}
    acquireCh chan *Resource
    releaseCh chan *Resource
}

func NewResourceManager() *ResourceManager {
    r := &ResourceManager{
        closeCh:   make(chan struct{}),
        acquireCh: make(chan *Resource),
        releaseCh: make(chan *Resource),
    }
    go r.run()
    return r
}

func (r *ResourceManager) run() {
    defer close(r.acquireCh)
    for {
        if len(r.resources) > 0 {
            select {
            case r.acquireCh <- r.resources[len(r.resources)-1]:
                r.resources = r.resources[:len(r.resources)-1]
            case res := <-r.releaseCh:
                r.resources = append(r.resources, res)
            case <-r.closeCh:
                return
            }
        } else {
            select {
            case res := <-r.releaseCh:
                r.resources = append(r.resources, res)
            case <-r.closeCh:
                return
            }
        }
    }
}

func (r *ResourceManager) AcquireResource() *Resource {
    return <-r.acquireCh
}

func (r *ResourceManager) ReleaseResource(res *Resource) {
    r.releaseCh <- res
}

func (r *ResourceManager) Close() {
    close(r.closeCh)
}

// small demo below ...

func test(id int, r *ResourceManager) {
    for {
        res := r.AcquireResource()
        fmt.Printf("test %d: %s\n", id, res.Data)
        time.Sleep(time.Millisecond)
        r.ReleaseResource(res)
    }
}

func main() {
    r := NewResourceManager()
    r.ReleaseResource(&Resource{"Resource A"}) // initial setup
    r.ReleaseResource(&Resource{"Resource B"}) // initial setup
    go test(1, r)
    go test(2, r)
    go test(3, r) // 3 consumers, but only 2 resources ...
    time.Sleep(time.Second)
    r.Close()
}

【讨论】:

  • 我喜欢这种方法,但我认为通道开销可能会减慢解决方案的速度。让我进行基准测试,看看它与互斥锁相比如何。
  • 当然,很想知道它的比较。至于更新的问题:是的,CAS 可以正常工作。一个CAS就是一个CAS。该解决方案的低效率在于不断迭代使用中的元素并有效地处理资源耗尽。
猜你喜欢
  • 1970-01-01
  • 2014-05-21
  • 2018-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多