并发安全是最基本的常识,也是最容易忽,同时也考验一个工程师 enginner 的语言基本功和代码规范。
并发访问修改变量,会导致各种不可预期的结果,最严重的就是程序 panic, 比如常见的 go 语言中 map concurrent read/write panic
先来讲几个例子,老生常谈的 case, 再说说如何避免
下面是一个 concurrent read/write string 的例子
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
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
呢?
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
Data uintptr
Len int
}
在 go 语言中,字符串是由结构体 StringHeader
表示的,源码中写的清楚非并发安全,如果读取字符串时,巧好有另外一个 goroutine 只更改了 uintptr 没修改 Len, 那就会出现如上问题。
再来举一个 error 接口的例子,来自我司 POI 团队。省去上下文,本质就是 error 变量并发修改导致的 panic
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 其实是一样的
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 语言里接口的定义
// 没有方法的interface
type eface struct {
_type *_type
data unsafe.Pointer
}
// 有方法的interface
type iface struct {
tab *itab
data unsafe.Pointer
}
道理是一模一样的,只要存在并发读写,就会出现所谓的 partial write, 结果就不可预期
fn main() {
let a = String::from("abc");
let b = a;
println!("{}", b);
println!("{}", a);
}
这是一段 rust 入门级代码,运行会报错:
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 学习曲线太陡。
分好多层面来讲这个事情
简单来讲,锁够了,早年的 leveldb 就是一把大锁撸遍全场
一把足矣,不够的话,就分段锁来个100把 ... 比如 statsd agent, 由于单个 agent 有把大锁,多创建几个 agent 就行了,同步不行换成异步 ...
很多代码都没有严苛到一把锁就严重降低性能的程序,为了程序的正确,切忌过早优化。尤其业务代码,性能不行 asg 扩容堆机器。
靠工具的 linter 提示能做到一些显示的检查,包括不规范的代码什么的,都是可以的。但毕竟不是 rust 编译器检查,其实编译器也并不是万能的。
打铁也要自身硬,以前 c/c++ 的程序员,每写一行代码,都知道传入传出的变量,是如何构造和析构的,否则内存泄漏了都不知道。
现在更高级的语言,内置 GC 带来了开发效率的提升,但不代表工程师可以不思考了。如果真是那样,是不是哪天 AI 就可以代替程序员了,好像是的...
可能这就是高级语言的不可能三角吧,开发效率、程序性能、运行时安全。听说抖音广告部门把 go 换成 c++ 后,解决了 latency long tail 问题,同时开发效率降低了四倍:(
这次分享就这些,以后面还会分享更多的内容。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8