为了账号安全,请及时绑定邮箱和手机立即绑定

在读取和修改之前锁定切片

在读取和修改之前锁定切片

Go
慕姐8265434 2023-02-21 12:40:26
我使用 Go 的经验是最近的,在审查一些代码时,我看到虽然它被写保护,但读取数据时存在问题。不是阅读本身,而是阅读和修改切片之间可能发生的修改。type ConcurrentSlice struct {    sync.RWMutex    items []Item}type Item struct {    Index int    Value Info}type Info struct {    Name        string     Labels      map[string]string    Failure     bool}如前所述,写作以这种方式受到保护:func (cs *ConcurrentSlice) UpdateOrAppend(item ScalingInfo) {    found := false    i := 0    for inList := range cs.Iter() {        if item.Name == inList.Value.Name{            cs.items[i] = item            found = true        }        i++    }    if !found {        cs.Lock()        defer cs.Unlock()        cs.items = append(cs.items, item)    }}func (cs *ConcurrentSlice) Iter() <-chan ConcurrentSliceItem {    c := make(chan ConcurrentSliceItem)    f := func() {        cs.Lock()        defer cs.Unlock()        for index, value := range cs.items {            c <- ConcurrentSliceItem{index, value}        }        close(c)    }    go f()    return c}但是在收集切片的内容和修改它之间,可能会发生修改。可能是另一个例程修改了同一个切片,当该赋值时,它已经不存在了:slice[i] = item处理这个问题的正确方法是什么?我已经实现了这个方法:func GetList() *ConcurrentSlice {    if list == nil {        denylist = NewConcurrentSlice()        return denylist    }    return denylist}我这样使用它:concurrentSlice := GetList()concurrentSlice.UpdateOrAppend(item)但我知道在获取和修改之间,即使它实际上是立即的,另一个例程也可能修改了切片。以原子方式执行这两个操作的正确方法是什么?我阅读的切片 100% 是我修改的切片。因为如果我试图将一个项目分配给一个不再存在的索引,它会中断执行。先感谢您!
查看完整描述

5 回答

?
幕布斯6054654

TA贡献1876条经验 获得超7个赞

您进行阻止的方式是不正确的,因为它不能确保您退回的物品没有被移除。在更新的情况下,数组仍将至少保持相同的长度。


一个更简单的可行解决方案如下:


func (cs *ConcurrentSlice) UpdateOrAppend(item ScalingInfo) {

    found := false

    i := 0

    cs.Lock()

    defer cs.Unlock()


    for _, it := range cs.items {

        if item.Name == it.Name{

            cs.items[i] = it

            found = true

        }

        i++

    }

    if !found {

        cs.items = append(cs.items, item)

    }

}


查看完整回答
反对 回复 2023-02-21
?
翻阅古今

TA贡献1780条经验 获得超5个赞

如果值的顺序不重要,请使用sync.Map 。

type Items struct {

    m sync.Map

}


func (items *Items) Update(item Info) {

    items.m.Store(item.Name, item)

}


func (items *Items) Range(f func(Info) bool) {

    items.m.Range(func(key, value any) bool {

        return f(value.(Info))

    })

}


查看完整回答
反对 回复 2023-02-21
?
慕哥6287543

TA贡献1831条经验 获得超10个赞

数据结构 101:始终为您的用例选择最佳数据结构。如果您要按名称查找对象,那正是 map 的用途。如果您仍然需要维护项目的顺序,则使用树图


并发 101:与事务一样,您的互斥量应该是原子的、一致的和隔离的。你在这里隔离失败,因为读取的数据结构不在你的互斥锁内。


您的代码应如下所示:


func {

 mutex.lock

 defer mutex.unlock

 check map or treemap for name

 if exists update

 else add

}


查看完整回答
反对 回复 2023-02-21
?
拉莫斯之舞

TA贡献1820条经验 获得超10个赞

经过一些测试,我可以说你担心的情况确实会发生sync.RWMutex。我认为它也可能发生sync.Mutex,但我无法重现。也许我遗漏了一些信息,或者调用是有序的,因为它们都被阻止了,并且它们赎回锁定权的顺序是以某种方式排序的。

在没有其他例程进入“冲突”的情况下保持两个调用安全的一种方法是为该对象上的每个任务使用另一个互斥锁。您将在读写之前锁定该互斥锁,并在完成后释放它。您还必须在写入(或读取)该对象的任何其他调用上使用该互斥锁。您可以在 main.go 文件中找到我所说内容的实现。为了重现 RWMutex 的问题,您可以简单地注释 startTask 和 endTask 调用,并且该问题在终端输出中可见。

编辑:我的第一个答案是错误的,因为我误解了测试结果,并陷入了 OP 描述的情况。


查看完整回答
反对 回复 2023-02-21
?
慕娘9325324

TA贡献1783条经验 获得超4个赞

如果ConcurrentSlice要从单个 goroutine 使用,则不需要锁,因为在那里编写的算法不会对 slice 元素或 slice 进行任何并发读/写。


如果ConcurrentSlice要从多个 goroutines 使用,现有的锁是不够的。这是因为UpdateOrAppend可能同时修改切片元素。


安全版本需要两个版本Iter:


这可以由 的用户调用ConcurrentSlice,但不能从 `UpdateOrAppend 中调用:


func (cs *ConcurrentSlice) Iter() <-chan ConcurrentSliceItem {

    c := make(chan ConcurrentSliceItem)


    f := func() {

        cs.RLock()

        defer cs.RUnlock()

        for index, value := range cs.items {

            c <- ConcurrentSliceItem{index, value}

        }

        close(c)

    }

    go f()


    return c

}

这只能从以下位置调用UpdateOrAppend:


func (cs *ConcurrentSlice) internalIter() <-chan ConcurrentSliceItem {

    c := make(chan ConcurrentSliceItem)


    f := func() {

        // No locking

        for index, value := range cs.items {

            c <- ConcurrentSliceItem{index, value}

        }

        close(c)

    }

    go f()


    return c

}

并且UpdateOrAppend应该在顶层同步:


func (cs *ConcurrentSlice) UpdateOrAppend(item ScalingInfo) {

cs.Lock()

defer cs.Unlock()

....

}

这是长版:


这是一段有趣的代码。Iter()根据我对 go 内存模型的理解,只有当有另一个 goroutine 在处理这段代码时才需要互斥锁,即使这样,代码中也可能存在竞争。但是,UpdateOrAppend只修改索引低于Iter正在处理的索引的切片元素,因此竞争永远不会表现出来。

比赛可以按如下方式进行:

  1. iter 中的 for 循环读取切片的元素 0

  2. 元素通过通道发送。因此,切片接收发生在第一步之后。

  3. 接收端可能会更新切片的元素 0。到这里没有问题。

  4. 然后发送 goroutine 读取切片的元素 1。这是比赛可以发生的时候。如果第 3 步更新了切片的索引 1,则第 4 步的读取是一场竞赛。即:如果第 3 步读取到第 4 步完成的更新,则这是一场比赛。如果您在 UpdateOrAppend 中以 i:=1 开始,并使用 -race 标志运行它,您可以看到这一点。

但是UpdateOrAppend总是修改 i=0 时已经看到的切片元素Iter,所以这段代码是安全的,即使没有锁。

如果有其他 goroutines 访问和修改结构,你需要 Mutex,但你需要它来保护完整的UpdateOrAppend方法,因为应该只允许一个 goroutine 运行它。您需要互斥锁来保护第一个 for 循环中的潜在更新,并且该互斥锁还必须包括切片追加情况,因为这实际上可能会修改底层对象的切片。

如果Iter仅调用 from UpdateOrAppend,那么这个单个互斥锁就足够了。但是,如果Iter可以从多个 goroutines 调用,那么还有另一种竞争可能性。如果一个实例UpdateOrAppend与多个实例并发运行Iter,那么其中一些Iter实例将同时从修改后的切片元素中读取,从而导致竞争。所以,应该是 multiple Iters 只有在没有调用的情况下才能运行UpdateOrAppend。那是一个 RWMutex。

但是可以从带锁的地方Iter调用,所以不能真的调用,否则就是死锁。UpdateOrAppendRLock

因此,您需要两个版本Iter:一个可以在外部调用UpdateOrAppend,在 goroutine 中发出RLock,另一个只能从中调用UpdateOrAppend,不能调用RLock


查看完整回答
反对 回复 2023-02-21
  • 5 回答
  • 0 关注
  • 95 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信