聊聊 Go 併發安全
併發安全是最基本的常識,也是最容易忽,同時也考驗一個工程師 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, 結果就不可預期
看看 rust
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 擴容堆機器。
CI/CD
靠工具的 linter 提示能做到一些顯示的檢查,包括不規範的代碼什麼的,都是可以的。但畢竟不是 rust 編譯器檢查,其實編譯器也並不是萬能的。
工程師
打鐵也要自身硬,以前 c/c++ 的程序員,每寫一行代碼,都知道傳入傳出的變量,是如何構造和析構的,否則內存泄漏了都不知道。
現在更高級的語言,內置 GC 帶來了開發效率的提升,但不代表工程師可以不思考了。如果真是那樣,是不是哪天 AI 就可以代替程序員了,好像是的...
可能這就是高級語言的不可能三角吧,開發效率、程序性能、運行時安全。聽說抖音廣告部門把 go 換成 c++ 後,解決了 latency long tail 問題,同時開發效率降低了四倍:(
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ybW8sW7Xq6XpqQzMQlIlHA