From 60d5f04cc1368b92ae79f267c47d546c6c489cb4 Mon Sep 17 00:00:00 2001 From: shgopher Date: Thu, 30 Nov 2023 23:45:25 +0800 Subject: [PATCH] feat(concurrency): add sync.Mutex:data race --- .../README.md" | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git "a/\345\271\266\345\217\221/\345\220\214\346\255\245\345\216\237\350\257\255/README.md" "b/\345\271\266\345\217\221/\345\220\214\346\255\245\345\216\237\350\257\255/README.md" index 1ffe6c1d2..a725c1d7d 100644 --- "a/\345\271\266\345\217\221/\345\220\214\346\255\245\345\216\237\350\257\255/README.md" +++ "b/\345\271\266\345\217\221/\345\220\214\346\255\245\345\216\237\350\257\255/README.md" @@ -2,7 +2,7 @@ * @Author: shgopher shgopher@gmail.com * @Date: 2023-05-14 23:08:19 * @LastEditors: shgopher shgopher@gmail.com - * @LastEditTime: 2023-11-29 22:14:21 + * @LastEditTime: 2023-11-30 23:43:50 * @FilePath: /GOFamily/并发/同步原语/README.md * @Description: * @@ -79,6 +79,81 @@ type Locker interface{ Unlock() } ``` +Mutex 就实现了这个 Locker 接口 + +Locker 定义了锁的基本方法,加锁 + 解锁 + +### 什么是 data race 的本质 +我们常说 data race 的情况,是多线程同时对于某块内存进行数据的变更,那么问题来了,这个地方谈到的同时,是真的物理层面的同时还是近似同时? + +关于 data race 中的 “同时” 通常是**指逻辑上的同时或近似同时,而不是物理层面严格的同一时刻。** + +主要原因有以下几点: + +- 现代 CPU 中,同一时刻真正执行指令的只有一个核心。不同核心之间以及同一核心的不同执行周期之间,不存在物理层面严格的同步。 +- 即使在单核 CPU 上,由于指令流水线、内存缓存、分支预测等机制,实际执行顺序也可能与代码顺序不一致,很难定义物理层面严格的同步。 +- 不同线程之间进行切换的时间间隔非常小 (几十到几百纳秒),对程序逻辑而言可以视为同时进行。 +- 要构成 data race,不同线程对同一地址的访问之间间隔不能太长,必须在一个指令/操作的启始和完成之间,所以也符合逻辑上的近似同时。 + +所以,data race 中的 “同时” 就是指逻辑上近似同时,或者无法确定准确执行顺序的情况,而不是物理层面严格同一时刻。这种近似同时从程序角度就是可能造成冲突,需要进行同步处理。 +### 检测 data race 的工具 +并发问题不是一定能肉眼看出来的,如果只是基础的,容易看出来的也就罢了,但是很多隐藏的 data race 问题必须使用专业的工具去鉴别,go 语言提供了 `-race` 功能,在编译,测试,run 的时候,会自动检测到 data race 问题,并且给出详细的错误信息。 + +```bash + go run -race main.go +``` + +我们看一个例子 +```go +func main() { + value := 0 + for i := 0; i < 10000; i++ { + go func(){ + value ++ + }() + } + time.Sleep(1000) + fmt.Println(value) +} +``` +本能的你会以为能输出 10000,但是结果确实 9000 多,而且还不一定,这是为啥呢? + +因为你以为 ++ 操作是原子操作,其实并不是。 + +++ 操作分为三个步骤 +- 获取 value 值 +- 值+1 +- 将新值写回 + +这其实是三个步骤,10000 个线程,假如同时有 10 个去读了这个 value,在他们看来都是初始值是 0,然后他们+1,然后写回去了结果 value 是 1,相当于 10 个 goroutine 都去写,本来应该是 10,但是赋值回去都变成了 1 + +这个时候,如果你使用 run -race 就能检测出来 + +```bash +go1 go run -race main.go +================== +WARNING: DATA RACE +Read at 0x00c00010a018 by goroutine 8: + main.main.func1() + /Users/shgopher/Desktop/1/go1/main.go:23 +0x2c + +Previous write at 0x00c00010a018 by goroutine 6: + main.main.func1() + /Users/shgopher/Desktop/1/go1/main.go:23 +0x3c + +Goroutine 8 (running) created at: + main.main() + /Users/shgopher/Desktop/1/go1/main.go:22 +0x48 + +Goroutine 6 (finished) created at: + main.main() + /Users/shgopher/Desktop/1/go1/main.go:22 +0x48 +================== +9957 +Found 1 data race(s) +exit status 66 +``` +多线程多某个区域的内存进行同时 (或者近似同时) 操作,这就是数据竞争 ### sync.Mutex 互斥锁的实现原理 go 语言互斥锁的实现非常简单,只有这一个结构体就是核心: