惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
O
OpenAI News
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
罗磊的独立博客
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
Hugging Face - Blog
Hugging Face - Blog
D
DataBreaches.Net
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
雷峰网
雷峰网
V
Visual Studio Blog
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
博客园 - 【当耐特】
G
Google Developers Blog
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
Martin Fowler
Martin Fowler
F
Fortinet All Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
AI
AI
Google Online Security Blog
Google Online Security Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

董泽润的技术笔记

美图 kv 存储 titan | 董泽润的技术笔记 聊聊并发库 conc | 董泽润的技术笔记 浅析 redis lua 实现 | 董泽润的技术笔记 IO Pipeline 读 Minio 源码 聊聊最近基于 S3 的项目 | 董泽润的技术笔记 新手如何调试 MySQL | 董泽润的技术笔记 为什么泛型使你的程序变慢 | 董泽润的技术笔记 每个 gopher 都需要了解的 Go AST 小技巧!Wireshark 让调试 GRPC 不再困难 | 董泽润的技术笔记 小技巧!Mac 环境下编译 Go 服务 | 董泽润的技术笔记 小技巧!k8s 环境下调试服务 | 董泽润的技术笔记 小技巧!如何用 systemtap 排查问题 | 董泽润的技术笔记 小白的 redis 1 安全漏洞 | 董泽润的技术笔记 聊聊时钟源为什么会影响性能 | 董泽润的技术笔记 聊聊为什么 IDL 只能扩展字段而非修改 | 董泽润的技术笔记 什么是 Pause 容器 | 董泽润的技术笔记 弱智的 MySQL NULL | 董泽润的技术笔记 分享一个 UT failed 引出的思考 | 董泽润的技术笔记 聊聊如何做技术分享 | 董泽润的技术笔记
聊聊 Go 并发安全 | 董泽润的技术笔记
2022-04-06 · via 董泽润的技术笔记

并发安全是最基本的常识,也是最容易忽视的前提,更考验一个工程师 enginner 的语言基本功和代码规范。

并发访问修改变量,会导致各种不可预期的结果,最严重的就是程序 panic, 比如常见的 go 语言中 map concurrent read/write panic

先来讲几个例子,老生常谈的 case, 再说说如何避免

字符串修改

下面是一个 concurrent read/write string 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import (
"fmt"
"time"
)
const (
FIRST = "WHAT THE"
SECOND = "F*CK"
)
func main() {
var s string
go func() {
i := 1
for {
i = 1 - i
if i == 0 {
s = FIRST
} else {
s = SECOND
}
time.Sleep(10)
}
}()
for {
if s == "WHAT" {
panic(s)
}
fmt.Println(s)
time.Sleep(10)
}
}

一个 goroutine 反复赋值变量 s, 同时另外 main 去读取变量 s, 如果发现字符串读到的是 “WHAT” 就主动 panic

1
2
3
4
5
6
7
8
WHAT THE
WHAT THE
panic: WHAT

goroutine 1 [running]:
main.main()
/Users/zerun.dong/code/gotest/string.go:26 +0x11a
exit status 2

上面代码运行后,注定要 panic, 代码的主观意愿是字符串赋值是原子的,要么是 F*CK, 要么是 WHAT THE, 为什么会出现 WHAT 呢?

1
2
3
4
5
6
7
8
9
10






type StringHeader struct {
Data uintptr
Len int
}

在 go 语言中,字符串是由结构体 StringHeader 表示的,源码中写的清楚非并发安全,如果读取字符串时,巧好有另外一个 goroutine 只更改了 uintptr 没修改 Len, 那就会出现如上问题。

接口

再来举一个 error 接口的例子,来自我司 POI 团队。省去上下文,本质就是 error 变量并发修改导致的 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"fmt"
"github.com/myteksi/hystrix-go/hystrix"
"time"
)
var FIRST error = hystrix.CircuitError{Message:"timeout"}
var SECOND error = nil
func main() {
var err error
go func() {
i := 1
for {
i = 1 - i
if i == 0 {
err = FIRST
} else {
err = SECOND
}
time.Sleep(10)
}
}()
for {
if err != nil {
fmt.Println(err.Error())
}
time.Sleep(10)
}
}

复现 case 其实是一样的

1
2
3
4
5
6
7
8
9
10
ITCN000312-MAC:gotest zerun.dong$ go run panic.go
hystrix: timeout
panic: value method github.com/myteksi/hystrix-go/hystrix.CircuitError.Error called using nil *CircuitError pointer

goroutine 1 [running]:
github.com/myteksi/hystrix-go/hystrix.(*CircuitError).Error(0x0, 0xc0000f4008, 0xc000088f40)
<autogenerated>:1 +0x86
main.main()
/Users/zerun.dong/code/gotest/panic.go:25 +0x82
exit status 2

来看一下 go 语言里接口的定义

1
2
3
4
5
6
7
8
9
10

type eface struct {
_type *_type
data unsafe.Pointer
}

type iface struct {
tab *itab
data unsafe.Pointer
}

道理是一模一样的,只要存在并发读写,就会出现所谓的 partial write

看看 rust

1
2
3
4
5
6
7
fn main() {
let a = String::from("abc");
let b = a;

println!("{}", b);
println!("{}", a);
}

这是一段 rust 入门级代码,运行会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ITCN000312-MAC:hello zerun.dong$ cargo run
Compiling hello v0.1.0 (/Users/zerun.dong/projects/hello)
error[E0382]: borrow of moved value: `a`
--> src/main.rs:6:20
|
2 | let a = String::from("abc");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
...
6 | println!("{}", a);
| ^ value borrowed here after move

error: aborting due to previous error

因为变量 a 己经被 move 走了,所以程序不可以再继续使用该变量。这就是 rust ownership 所有权的概念。在编译器层面就避免了上面提到的问题,当然 rust 学习曲线太陡。

如何保证安全

分好多层面来讲这个事情

语言

简单来讲,一把大锁足矣,一把不够,就分段锁来个100把 … 比如 statsd agent, 由于单个 agent 有把大锁,多创建几个 agent 就行了,同步不行换成异步 …

很多代码都没有严苛到一把锁就严重降低性能的程序,为了程序的正确,切忌过早优化。尤其业务代码,性能不行 asg 扩容堆机器。

CI/CD

靠工具的 linter 提示能做到一些显示的检查,包括不规范的代码什么的,都是可以的。但毕竟不是 rust 编译器检查,其实编译器也并不是万能的。

工程师

当然打铁也要自身硬,以前 c/c++ 的程序员,每写一行代码,都知道传入传出的变量,是如何构造和析构的,否则内存泄漏了都不知道。

现在更高级的语言,自带 gc 带来了开发效率的提升,但不代表工程师可以不思考了。如果真是那样,是不是哪天 AI 就可以代替程序员了,好像是的…

可能这就是高级语言的不可能三角吧,开发效率、心智负担、运行时安全

小结

这次分享就这些,以后面还会分享更多的内容,如果感兴趣,可以关注并点击左下角的分享转发哦(:

s