Go并发读取string的Panic问题

Posted 耳冉鹅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go并发读取string的Panic问题相关的知识,希望对你有一定的参考价值。

上问题,先看下panic的函数栈信息,说现实strings.Count()发生了panic,来看下函数

第一个参数是字符串s,再结合函数栈信息的十六进制,0x00x9表示字符串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)
	


StringHeaderstring的数据结构是一样的,只不过参数可以导出,方便打印

这个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问题的主要内容,如果未能解决你的问题,请参考以下文章

009-Go 读取写入CSV文件

go 错误处理panic recover

go语言之抛出异常

深度解密 Go 语言之 sync.map

GO语言异常处理机制panic和recover分析

并发安全的 map:sync.Map源码分析