i++为什么不是原子操作

从处理器层面上来讲,处理器保证基本的访存事务的原子性,例如当处理器读取存储器中的一个字节时,在读取过程未结束之前,其他的任何设备都不可以访问这个字节。这个保证对写入字节也成立。但是处理器自动能做的保护也就仅仅如此了。

问题描述

以下面的Go代码为例,我们对x累加了10000次,但是最终x的结果却并不为10000:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var x int
    var wg sync.WaitGroup

    fmt.Println("x start: ", x)

    wg.Add(10000)
    for i := 0; i < 10000; i++ {
        go func() {
            x++
            wg.Done()
        }()
    }
    wg.Wait()

    fmt.Println("x end:", x)
}

i++不是原子操作

对于i++这样的操作,其实是分3步执行的,读取i的值,增加i的值,回写i的新值。这3步每一步都是原子操作,但是组合在一起就不一定是原子操作了。可以参看下图中两个线程对i的争抢示例。我们期待线程A与B中的i++可以顺序执行,最终存储器中的结果是2;但是实际上由于两个线程并行执行,结果可能得到1(当然也有可能得到2,运气好的话)。通常i++这样的操作我们称为“读-改-写”操作。

解决方法

package main

import (
    "fmt"
    "sync"
)

func main() {
    var x int
    var lock sync.Mutex
    var wg sync.WaitGroup

    fmt.Println("x start: ", x)

    wg.Add(10000)
    for i := 0; i < 10000; i++ {
        go func() {
            lock.Lock()
            x++
            lock.Unlock()
            wg.Done()
        }()
    }
    wg.Wait()

    fmt.Println("x end:", x)
}

由于的确有业务逻辑要求多线程中类似于i++这样的“读-改-写”操作是原子性操作。处理器因此为软件(即程序员/编译器)提供了一种原生的总线封锁机制,即lock指令前缀。当生成的代码前有lock前缀时,例如伪代码lock i++,则整条指令在访存/修改变量/回写期间,别的设备/代码都不可以触碰被锁住的变量。从上面的例子来看就是,一旦线程A进入load i阶段,线程B就不可以再执行load i。线程B会阻塞,一直等待到线程A的store i完成,才能继续往下执行load i(此时读取到的是线程A的结果i=1),i++, store i,最后将i=2写回到内存中。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇