Go并发读取string的Panic问题

在这里插入图片描述

上问题,先看下panic的函数栈信息,说现实strings.Count()发生了panic,来看下函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygXDobAe-1675330949311)(assets/IMG_15.png)]

第一个参数是字符串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地址的一致性,完结啦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值