duanlei2150 2017-11-22 23:21
浏览 65
已采纳

使用原子操作的计数器和使用互斥量的计数器之间的Go区别吗?

I have seen some discussion lately about whether there is a difference between a counter implemented using atomic increment/load, and one using a mutex to synchronise increment/load.

Are the following counter implementations functionally equivalent?

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
}

I have run a bunch of test cases (Playground Link) and haven't been able to see any different behaviour. Running the tests on my machine the numbers get printed out of order for all the PrintAll test functions.

Can someone confirm whether they are equivalent or if there are any edge cases where these are different? Is there a preference to use one technique over the other? The atomic documentation does say it should only be used in special cases.

Update: The original question that caused me to ask this was this one, however it is now on hold, and i feel this aspect deserves its own discussion. In the answers it seemed that using a mutex would guarantee correct results, whereas atomics might not, specifically if the program is running in multiple threads. My questions are:

  • Is it correct that they can produce different results? (See update below. The answer is yes.).
  • What causes this behaviour?
  • What are the tradeoffs between the two approaches?

Another Update:

I've found some code where the two counters behave differently. When run on my machine this function will finish with MutexCounter, but not with AtomicCounter. Don't ask me why you would ever run this code:

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
}
  • 写回答

3条回答 默认 最新

  • dqch34769 2017-11-29 02:24
    关注

    Alright, I'm going to attempt to self-answer for some closure. Edits are welcome.

    There is some discussion about the atomic package here. But to quote the most telling comments:

    The very short summary is that if you have to ask, you should probably avoid the package. Or, read the atomic operations chapter of the C++11 standard; if you understand how to use those operations safely in C++, then you are more than capable of using Go's sync/atomic package.

    That said, sticking to atomic.AddInt32 and atomic.LoadInt32 is safe as long as you are just reporting statistical information, and not actually relying on the values carrying any meaning about the state of the different goroutines.

    And:

    What atomicity does not guarantee, is any ordering of observability of values. I mean, atomic.AddInt32() does only guarantee that what this operation stores at &cnt will be exactly *cnt + 1 (with the value of *cnt being what the CPU executing the active goroutine fetched from memory when the operation started); it does not provide any guarantee that if another goroutine will attempt to read this value at the same time it will fetch that same value *cnt + 1.

    On the other hand, mutexes and channels guarantee strict ordering of accesses to values being shared/passed around (subject to the rules of Go memory model).

    In regards to why the code sample in the question never finishes, this is due to fact that the func that is reading the counter is in a very tight loop. When using the atomic counter, there are no syncronisation events (e.g. mutex lock/unlock, syscalls) which means that the goroutine never yields control. The result of this is that this goroutine starves the thread it is running on, and prevents the scheduler from allocating time to any other goroutines allocated to that thread, this includes ones that increment the counter meaning the counter never reaches 10000000.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(2条)

报告相同问题?

悬赏问题

  • ¥15 Python时间序列如何拟合疏系数模型
  • ¥15 求学软件的前人们指明方向🥺
  • ¥50 如何增强飞上天的树莓派的热点信号强度,以使得笔记本可以在地面实现远程桌面连接
  • ¥15 MCNP里如何定义多个源?
  • ¥20 双层网络上信息-疾病传播
  • ¥50 paddlepaddle pinn
  • ¥20 idea运行测试代码报错问题
  • ¥15 网络监控:网络故障告警通知
  • ¥15 django项目运行报编码错误
  • ¥15 STM32驱动继电器