我最近看到一些关于使用原子增量/负载实现的计数器与使用互斥量来同步增量/负载的计数器之间是否存在差异的一些讨论.
以下计数器实现在功能上是否相同?
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 次 |
最近记录: |