duanlei2150 2017-11-22 15: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-28 18: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 MATLAB解决问题
  • ¥20 哪位专业人士知道这是什么原件吗?哪里可以买到?
  • ¥15 关于#c##的问题:treenode反序列化后获取不到上一节点和下一节点,Fullpath和Handle报错
  • ¥15 一部手机能否同时用不同的app进入不同的直播间?
  • ¥15 没输出运行不了什么问题
  • ¥20 输入import torch显示Intel MKL FATAL ERROR,系统驱动1%,: Cannot load mkl_intel_thread.dll.
  • ¥15 点云密度大则包围盒小
  • ¥15 nginx使用nfs进行服务器的数据共享
  • ¥15 C#i编程中so-ir-192编码的字符集转码UTF8问题
  • ¥15 51嵌入式入门按键小项目