聊聊并发安全

207次阅读  |  发布于3年以前

并发安全是最基本的常识,也是最容易忽,同时也考验一个工程师 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 问题,同时开发效率降低了四倍:(

小结

这次分享就这些,以后面还会分享更多的内容。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8