Go并发读取string的Panic问题
Posted 耳冉鹅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go并发读取string的Panic问题相关的知识,希望对你有一定的参考价值。
上问题,先看下panic的函数栈信息,说现实strings.Count()发生了panic,来看下函数
第一个参数是字符串s
,再结合函数栈信息的十六进制,0x0
、0x9
表示字符串s的地址和长度
这里来看一下string的底层数据结构:
简单的结构,str是字符串的首地址
,len是字符串的长度
,string的数据结构与切片相似。struct的赋值并非是并发安全的,所以问题的现象也容易解释
0x0
表示nil,但上一个字符串的长度为0x9
,在读取或复制这个字符串的时候,刚好另一个goRoutine只更改了str没有修改len,这时候会出现上述现象:空字符串的长度为9,最终在bytealg.CountString()发生panic。
写个Demo复现一下
const (
BusinessOne = "what's up"
BusinessTwo = ""
)
func split(s string)
fmt.Printf("%+v\\n", *(*reflect.StringHeader)(unsafe.Pointer(&s)))
ss := strings.Split(s, ",")
fmt.Println(ss)
func main()
var str string
go func()
flag := false
for
if flag
str = BusinessOne
else
str = BusinessTwo
time.Sleep(10)
flag = !flag
()
for
split(str)
time.Sleep(10)
StringHeader
、string
的数据结构是一样的,只不过参数可以导出,方便打印
这个Demo也可以做一些修改, 比如在goRoutine中反复给s赋值长度不同且非零的字符串,然后再main中打印,就会发现长字符串偶现被截断的情况,被截断的长度正好是短字符串的长度。将上个Demo中BusinessTwo
修改即可
典型数据竟态 Case对应第四种
- 循环内并发竟态计数
- 意外共享变量
- 为保护全局变量读写(通常是map并发安全问题)
- 一些原子类型(通常是在结构体内变量并发读写)
- 向关闭通道发送数据
解决方案:
A typical fix for this race is to use a channel or a mutex. To preserve the lock-free behavior, one can also use the sync/atomic package.
// error case
type Watchdog struct last int64
func (w *Watchdog) KeepAlive()
w.last = time.Now().UnixNano() // First conflicting access.
func (w *Watchdog) Start()
go func()
for
time.Sleep(time.Second)
// Second conflicting access.
if w.last < time.Now().Add(-10*time.Second).UnixNano()
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
()
// ok
// 通过sync/atomic包无锁
type Watchdog struct last int64
func (w *Watchdog) KeepAlive()
atomic.StoreInt64(&w.last, time.Now().UnixNano())
func (w *Watchdog) Start()
go func()
for
time.Sleep(time.Second)
if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano()
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
()
然后开始研究本次问题的解决方案。本次问题出现的原因是string的修改并非是原子操作,与int、bool等不同,所以与数据竟态给的Demo不尽相同。大体思路是一致的,通过atomic的Store方法修改值,Load方法获取值,这样能够保证在split方法中打印出的指针内容一致。附上代码:
tmp用于获取string的指针地址
package main
import (
"fmt"
"reflect"
"sync/atomic"
"time"
"unsafe"
)
const (
BusinessOne = "what's up"
BusinessTwo = ""
)
var PL *string
func split(s string)
fmt.Printf("%+v\\n", *(*reflect.StringHeader)(unsafe.Pointer(&s)))
tmp := (*unsafe.Pointer)(unsafe.Pointer(&PL))
str := (*string)(atomic.LoadPointer(tmp))
fmt.Println(str)
func main()
var str string
str = BusinessOne
go func()
flag := true
for
if flag
str = BusinessOne
tmp := (*unsafe.Pointer)(unsafe.Pointer(&PL))
atomic.StorePointer(tmp, unsafe.Pointer(&str))
else
str = BusinessTwo
tmp := (*unsafe.Pointer)(unsafe.Pointer(&PL))
atomic.StorePointer(tmp, unsafe.Pointer(&str))
time.Sleep(10)
flag = !flag
()
for
split(str)
time.Sleep(10)
可以看到string数据结构的一致性以及str地址的一致性,完结啦
以上是关于Go并发读取string的Panic问题的主要内容,如果未能解决你的问题,请参考以下文章