原子性
定义:一个操作或多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在多线程并发编程中,由于多个线程可能同时访问同一变量,如果没有保证原子性,就会出现多个线程同时修改同一变量的情况,从而导致数据出现错误,即竞态条件。
为了避免竞态条件的发生,我们需要使用原子操作来保证对变量的操作都是原子性的。原子操作在执行过程中不会被中断,具有排他性,即同一时刻只有一个线程可以执行原子操作,从而避免的多个线程同时修改同一变量的情况。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var num int32 = 0
var delta int32 = 1
// 原子性地增加 num 的值
atomic.AddInt32(&num, delta)
fmt.Println(num) // 输出 1
}
可见性
定义:一个线程对共享变量的修改对于其他线程是可见的。
由于每个线程都有自己的本地缓存(线程读取变量时,先从内存中读取到缓存,并在缓存中修改),所以一个线程对共享变量的修改可能不会立即被其他线程看到,这就可能导致其它线程读取到的是修改前的变量值,而不是修改后的变量值(还在缓存中)。这被称为脏读。
下面是一个使用 Mutex 保证可见性的例子:
package main
import (
"fmt"
"sync"
)
var num int
var mutex sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mutex.Lock()
num++
mutex.Unlock()
}()
}
wg.Wait()
fmt.Println(num) // 输出 10
}
锁的机制会带来一定的开销(并行:多人同行的通道,锁:通道只允许单人通过)
有序性
为了优化程序性能,编译器、处理器和运行时会对代码指令进行重排,重排过程依照无拓扑关系的原则可能会对语句进行打乱顺序。
finish := false
func test(){
...
val := Node{} // 语句一,新建一个对象
finish = true // 语句二
}
在上面的代码中,语句一与语句二无拓扑关系,因此编译器可能会对其重排为:
finish = true // 语句二
val := Node{} // 语句一
那么如果我们对test函数启用多个协程处理,并且存在另一个函数:
func check(){
if finish{
...
}
}
就可能会出现finish已经为true,但是val还没有新建对象的情况,此时调用val,比如说它有一个内置函数,调用该函数就会出错。
脏读
B修改数据,但还未最终提交到数据库,这是A来读了,读到了B修改的数据。但是B发现出错了,进行事务回滚,此时A读到的就是错误的数据。
不可重复读
A为了保证读取数据的准确性,前后读取两次判断是否出错。A读完第一次后,B对该数据修改了,A又读了一次,发现读的不一样,这就叫不可重复读
幻读
A想插入一条数据。在读读下,A读了两次数据,B在A读第一次后,往数据表里插入了同一条数据,因此,A读了两次,都没发现数据表里有这条数据,于是A也插入这条数据,但是报错了,提示插入失败。这就叫幻读。