【问题标题】:When should you use a mutex over a channel?什么时候应该在通道上使用互斥锁?
【发布时间】:2022-01-07 12:52:12
【问题描述】:

在过去的几周里,我一直在努力解决一个(不那么)简单的问题:

什么时候最好使用sync.Mutex,相反,什么时候最好使用chan

似乎对于很多问题,任何一种策略都可以与另一种策略互换 - 这就是问题所在!

使用 Golang 文档中的 this video。下面,我冒昧地在操场上指定代码并将其转换为 sync.Mutex 等效项。

是否存在某种问题(在现实世界中遇到)需要相互使用?

注意事项:

  • 我是this use of chan 的忠实粉丝,我很难想出一个使用sync.Mutex 的更优雅的实现。
  • 值得注意的是,chan 实现可以同时完成更多工作(达到 12 个)*

游乐场:

chan 打乒乓球:

package main

import (
    "fmt"
    "time"
)

type Ball struct { hits int }

func main() {
    table := make(chan *Ball)
    go player("ping", table)
    go player("pong", table)

    table <- new(Ball)
    time.Sleep(1 * time.Second)
    <-table
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- ball
    }
}

sync.Mutex 打乒乓球:

package main

import (
    "fmt"
    "time"
    "sync"
)

type Ball struct { hits int }

var m =  sync.Mutex{}

func main() {
    ball := new(Ball)
    go player("ping", ball)
    go player("pong", ball)

    time.Sleep(1 * time.Second)
}

func player(name string, ball *Ball) {
    for {
        m.Lock()
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        m.Unlock()

    }
}

【问题讨论】:

  • 这些是不同的工具。互斥锁对资源的访问顺序化。另一方面,通道用于协调 goroutine 之间的计算。当我们谈论 Mutex 时,可以使用毫无意义的通道来完成大量模式。例如,当我们关闭通道时,通道可以帮助构建将停止的管道。或者像扇入和扇出这样的模式。或者有 M:N 的工作派遣。
  • 您设计的与两者相对相似的示例是规则的例外。通常很清楚您需要“互斥”而不是同步和通信。当有疑问时,更简单的实现通常是您想要的。
  • 您还应该注意,您的示例实现不会做同样的事情。通道实现协调“乒乓”来回发送消息,而互斥锁实现只是提供围绕球变量的互斥,这导致没有确定性的排序。

标签: go concurrency


【解决方案1】:

在 Go 中,通道非常棒,您可以使用它们在 goroutine 之间进行通信。但是,为方便起见,您可能希望在某些情况下使用sync.Mutex。 这些情况如下:

  • 保护内部状态
  • 缓存问题
  • 为了更好的性能

这里有三个examples and explanations

  1. 一个简单的计数器

  1. 乒乓球比赛

  1. 最简单的缓存

【讨论】:

  • 惊人的答案!谢谢你!
  • 完美答案。
【解决方案2】:

在大多数情况下更喜欢频道,在某些情况下sync.Mutex:
在以下情况下使用频道:
- 转移所有权(转移数据本身而不是指向数据的指针)
- 协调和同步

在以下情况下使用原语:
- 保护结构的内部状态
- 性能关键(取决于算法和用例,不是一般规则)

注意:对于使用原语的并发安全代码需要更加小心。


微基准

为了清楚起见,假设我们需要一个一秒计数器,所以在下面的示例中,我们计数一秒然后打印计数器值以查看它的计数速度:

No |         Count |       Method
------------------------------------------------------
 1 |     17_729_027 | Using sync.RWMutex for increment   
 2 |     12_180_741 | Using channel for increment    
 3 |    106_743_095 | Using channel for timer 
 4 |    104_178_671 | Using time.AfterFunc and channel sync

注意:go版本go1.13.5 linux/amd64


代码:

1 - 使用sync.RWMutex 进行增量:

package main

import (
    "sync"
    "time"
)

func main() {
    var i rwm
    go func() {
        for {
            i.inc() // free running counter
        }
    }()
    time.Sleep(1 * time.Second)
    println(i.read()) // sampling the counter
}

type rwm struct {
    sync.RWMutex
    i int
}

func (l *rwm) inc() {
    l.Lock()
    defer l.Unlock()
    l.i++
}
func (l *rwm) read() int {
    l.RLock()
    defer l.RUnlock()
    return l.i
}

2 - 使用通道进行增量:

package main

import (
    "time"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    timeout := time.NewTimer(1 * time.Second)
loop:
    for {
        select {
        case <-timeout.C:
            timeout.Stop()
            break loop
        default:
            ch <- 1 + <-ch
        }
    }

    println(<-ch)
}

3 - 使用定时器通道:

package main

import "time"

func main() {
    ch := make(chan int)
    go func() {
        timeout := time.NewTimer(1 * time.Second)
        defer timeout.Stop()
        i := 1
        for {
            select {
            case <-timeout.C:
                ch <- i
                return
            default:
                i++
            }
        }
    }()

    println(<-ch)
}

4 - 使用time.AfterFunc 和频道同步:

package main

import (
    "fmt"
    "time"
)

func main() {
    d := 1 * time.Second
    i := uint64(0)
    ch := make(chan struct{})

    time.AfterFunc(d, func() {
        close(ch)
    })

loop:
    for {
        select {
        case <-ch:
            break loop
        default:
            i++
        }
    }

    fmt.Println(i) // 104_178_671
}

【讨论】:

  • 对不起,我不明白你在比较什么。第一个示例在每个增量上调用 Lock()。第二个示例不使用通道进行增量,它仅使用通道来检查计时器。通过通道传递每个值会更公平(这会使通道慢 2-4 倍)-example
  • 查看新的编辑,这是一个完整的重写,只为您的评论,感谢您的评论。除非使用通道进行同步或锁定增量,否则似乎没有其他并发安全方式来增加。
猜你喜欢
  • 2011-05-01
  • 2011-08-17
  • 2018-11-15
  • 2011-09-08
  • 2011-09-30
  • 1970-01-01
  • 2017-12-10
  • 2021-01-15
相关资源
最近更新 更多