我最近看到一些关于使用原子增量/负载实现的计数器与使用互斥量来同步增量/负载的计数器之间是否存在差异的一些讨论.
以下计数器实现在功能上是否相同?
type Counter interface {
Inc()
Load() int64
}
// Atomic Implementation
type AtomicCounter struct {
counter int64
}
func (c *AtomicCounter) Inc() {
atomic.AddInt64(&c.counter, 1)
}
func (c *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&c.counter)
}
// Mutex Implementation
type MutexCounter struct {
counter int64
lock sync.Mutex
}
func (c *MutexCounter) Inc() {
c.lock.Lock()
defer c.lock.Unlock()
c.counter++
}
func (c *MutexCounter) Load() int64 {
c.lock.Lock()
defer c.lock.Unlock()
return c.counter
}
Run Code Online (Sandbox Code Playgroud)
我运行了一堆测试用例(Playground Link)并且无法看到任何不同的行为.在我的机器上运行测试时,所有PrintAll测试功能都会无序打印数字.
有人可以确认它们是否相同或者是否存在不同的边缘情况?是否优先使用一种技术而不是另一种?该原子文档不说,它应该只在特殊情况下使用.
更新: 导致我问这个问题的原始问题就是这个问题,但它现在暂停了,我觉得这方面值得自己讨论.在答案中,似乎使用互斥锁可以保证正确的结果,而原子可能不会,特别是如果程序在多个线程中运行.我的问题是:
另一个更新:
我发现了一些代码,其中两个计数器的行为不同.在我的机器上运行时,此功能将完成MutexCounter,但不是AtomicCounter.不要问我为什么要运行这段代码:
func TestCounter(counter Counter) {
end := make(chan interface{})
for i := 0; i < 1000; i++ {
go func() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for j := 0; j < 10000; j++ {
k := int64(r.Uint32())
if k >= 0 {
counter.Inc()
}
}
}()
}
go func() {
prevValue := int64(0)
for counter.Load() != 10000000 { // Sometimes this condition is never met with AtomicCounter.
val := counter.Load()
if val%1000000 == 0 && val != prevValue {
prevValue = val
}
}
end <- true
fmt.Println("Count:", counter.Load())
}()
<-end
}
Run Code Online (Sandbox Code Playgroud)
kos*_*tix 10
原子在常见情况下更快:编译器将从包中对函数的每个调用转换为sync/atomic一组特殊的机器指令,这些指令基本上在 CPU 级别 \xe2\x80\x94 上运行,例如,在 x86\n 架构上,atomic.AddInt64将是转换为一些ADD以指令为前缀的普通\n类指令LOCK(参见示例)\xe2\x80\x94\n,后者确保系统中所有CPU上更新的内存位置\n的一致视图。
互斥体是一件非常复杂的事情,因为它最终包装了本机操作系统特定线程同步 API 的一些位(例如,在 Linux 上futex)。
另一方面,当涉及同步内容时,Go 运行时得到了相当多的优化(考虑到 Go 的主要卖点之一,这有点令人期待),并且互斥体实现试图避免如果可能的话,使用内核在 goroutine 之间执行同步,并完全在 Go 运行时本身中执行。
\n\n如果互斥锁的争用相当低,这可能会解释基准测试中\n的时间没有明显差异。
\n\n尽管如此,我仍然觉得有必要指出 \xe2\x80\x94 以防万一 \xe2\x80\x94 原子和\n更高级别的同步设施旨在解决不同的\n任务。比如说,在一般情况下,在整个函数 \xe2\x80\x94 甚至单个语句的执行过程中,你不能使用原子来保护某些内存状态。
\n行为没有区别.性能有所不同.
由于设置和拆卸,并且由于它们在锁定期间阻止其他goroutine,因此互斥体很慢.
原子操作很快,因为它们使用原子CPU指令,而不是依赖外部锁.
因此,只要可行,原子操作应该是首选.
好吧,我将尝试自我解答以解决问题。欢迎进行编辑。
有一个关于原子包一些讨论在这里。但是引用最有说服力的评论:
简短的摘要是,如果您必须询问,则应避免使用该包装。或者,阅读C ++ 11标准的“原子操作”一章;如果您了解如何在C ++中安全地使用这些操作,那么您将不仅仅可以使用Go的
sync/atomic软件包。就是说,只要您只是报告统计信息即可,坚持
atomic.AddInt32并保持atomic.LoadInt32安全,并且实际上不依赖于带有不同goroutine状态的任何含义的值。
和:
原子性不能保证的是值的可观察性的任何顺序。我的意思是,
atomic.AddInt32()仅保证此操作存储在&cnt上的内容是准确的*cnt + 1(值*cnt是当操作开始时执行从内存中获取活动goroutine的CPU 的值);它不提供任何保证,如果另一个goroutine将尝试同时读取此值,则它将获取该相同的值*cnt + 1。另一方面,互斥锁和通道可确保对共享/传递的值的访问进行严格排序(取决于Go内存模型的规则)。
关于问题中的代码示例为何从未完成的原因,这是由于func正在读取计数器的代码处于非常紧密的循环中。使用原子计数器时,没有同步事件(例如,mutex锁定/解锁,系统调用),这意味着goroutine永远不会产生控制权。这样做的结果是,此goroutine使正在运行的线程饿死,并阻止调度程序将时间分配给分配给该线程的任何其他goroutine,这包括使计数器递增的计数器,这意味着计数器永远不会达到10000000。
| 归档时间: |
|
| 查看次数: |
2663 次 |
| 最近记录: |