下面这段代码在长时间运行后,有一定的机率会出错,RandString(32)返回的全是 0.
从网上查的资料全局变量应该不会被回收才对。
package helper
import (
"math/rand"
"time"
)
const _charsetRand = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$"
var _seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
// RandStringWithCharset rand string with charset
func RandStringWithCharset(length int, charset string) string {
b := make([]byte, length)
l := len(charset)
for i := range b {
b[i] = charset[_seededRand.Intn(l)]
}
return string(b)
}
// RandString rand string
func RandString(length int) string {
return RandStringWithCharset(length, _charsetRand)
}
// RandInt rand int between [min, max)
func RandInt(min int, max int) int {
if min <= 0 || max <= 0 {
return 0
}
if min >= max {
return max
}
return _seededRand.Intn(max-min) + min
}
// RandMax rand int between [0, max)
func RandMax(max int) int {
if max <= 1 {
return 0
}
return _seededRand.Intn(max)
}
感谢大家的讨论,谢谢。
#2 b[i] = charset[createRand().Intn(l)] 这种写法在出问题的测试服务器里跑了1个多月,客户没有再报告问题。没有在产品服务器上用过这样的写法。
#41 说 RandString(32) 返回的全是 0 可能不是正确的,应该说是我们发现了大量为 32 个 0 的 token, 有可能有其他的情况我们没有注意到。
#25 #28 #39 我们的上层代码确实有 recover, 但只用来记录日志。
我们的 ServeHTTP 里的第一行就是:defer app.recv(w, r)
func (app *Application) recv(w http.ResponseWriter, r *http.Request) {
if rcv := recover(); rcv != nil {
w.WriteHeader(http.StatusInternalServerError)
if app.panic != nil {
app.panic(w, r, rcv)
} else {
app.logf("%s %s %s %s rcv: %v", r.RemoteAddr, r.Host, r.Method, r.URL.Path, rcv)
}
}
}
客户的服务器和测试服务器都是在符合PCI标准的机房里的,我们做外包的很难直接接触。
换成 #7 下面这样的代码不会有问题,目前来说我还没办法证明出现这个问题的具体原因,可能是某些特殊的seed导致的问题。
// RandomString random string
func RandomString(len int) (string, error) {
b := make([]byte, len/2)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
1
kneo 237 天前 via Android
不是线程安全的吧。
|
2
afxcn OP @kneo 应该和线程安全没什么关系吧,我也不确定。
换成下面的代码这样,就不会出现返回全为 0 的情况了。 ```go func createRand() *rand.Rand { if _seededRand == nil { _seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) } return _seededRand } // RandStringWithCharset rand string with charset func RandStringWithCharset(length int, charset string) string { b := make([]byte, length) l := len(charset) for i := range b { b[i] = charset[createRand().Intn(l)] } return string(b) } ``` |
4
pathletboy 237 天前
这么改试试
```go package helper import ( "math/rand" "time" ) const _charsetRand = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$" var _seededRand *rand.Rand func init() { _seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) } // RandStringWithCharset rand string with charset func RandStringWithCharset(length int, charset string) string { b := make([]byte, length) l := len(charset) for i := range b { b[i] = charset[_seededRand.Intn(l)] } return string(b) } // RandString rand string func RandString(length int) string { return RandStringWithCharset(length, _charsetRand) } // RandInt rand int between [min, max) func RandInt(min int, max int) int { if min <= 0 || max <= 0 { return 0 } if min >= max { return max } return _seededRand.Intn(max-min) + min } // RandMax rand int between [0, max) func RandMax(max int) int { if max <= 1 { return 0 } return _seededRand.Intn(max) } ``` |
7
afxcn OP @AceGo 换了写法后,好几年了,没出现过这种情况,不过后来我们又改了,改成用 crypto/rand 了,所以也不是 100%确定#2 的写法是不是对的。
```go package utils import ( "crypto/rand" "encoding/hex" ) // RandomString random string func RandomString(len int) (string, error) { b := make([]byte, len/2) _, err := rand.Read(b) if err != nil { return "", err } return hex.EncodeToString(b), nil } ``` |
9
yin1999 237 天前 via Android
现在最新版本 Go 里面,rand 包的全局随机数生成器的随机种子也是每次自动生成的了,而且有自带的加速特性,可以考虑切回 rand 的全局随机数生成器试试会不会有这个问题。
|
10
R18 237 天前
目前最新的 Go 版本也会出错吗?
|
11
dododada 237 天前
chatgpt
这段代码的问题在于并发访问了全局的 _seededRand 变量,导致了竞争条件( race condition )。在多个 goroutine 同时调用 RandStringWithCharset 函数时,它们可能会同时访问和修改 _seededRand ,从而导致不可预测的结果,甚至造成程序崩溃。 在第一段代码中,_seededRand 被多个 goroutine 同时访问和修改,因为没有对其进行同步操作或者使用互斥锁。而在第二段代码中,通过在 RandStringWithCharset 函数中调用 createRand 函数,每次都创建一个新的 _seededRand 实例,避免了并发访问全局变量的问题。 通过这样的修改,确保了在并发情况下每个 goroutine 都有自己的 _seededRand 实例,从而解决了竞争条件问题,确保了程序的稳定性。 |
12
ixiaohei 237 天前
```rand.NewSource(time.Now().UnixNano())```不是线程安全的。并发情况下会出现一些未定义的异常,比如 panic
|
13
R18 237 天前
```go
charset[_seededRand.Intn(l)] ``` 我有点好奇,就算不是线程安全的,这代码也不该返回字符串"0"啊,"0"在整个 charset 里也没有处在一个特殊的位置。 |
14
keakon 237 天前
https://pkg.go.dev/math/rand#NewSource
Unlike the default Source used by top-level functions, this source is not safe for concurrent use by multiple goroutines. 现在不需要手动初始化 seed 的 |
18
hxzhouh1 237 天前
up 能说一下复现的方式跟 go 版本嘛? 我目前没法复现
|
22
cloudzhou 237 天前
https://pkg.go.dev/math/rand#NewSource
NewSource returns a new pseudo-random Source seeded with the given value. Unlike the default Source used by top-level functions, this source is not safe for concurrent use by multiple goroutines. The returned Source implements Source64. |
23
afxcn OP @hxzhouh1 很难重现了,21 年的事情,应该是当时最新版本的 go ,但我当时在自己的电脑跑,都没办法重现。
只有在测试服务器上出现,而且不是一次;第一次出现的时候以为是服务器被黑了,后来才定位到这段代码。 |
24
cloudzhou 237 天前
/*
* Top-level convenience functions */ var globalRand = New(&lockedSource{src: NewSource(1).(Source64)}) |
25
zhaoxueqin248 237 天前 1
@afxcn 这个 math/rand 的 IntN 在搞并发下运行会有概率 panic 的, 是不是上层有 recover 默认值 导致的 0 ?
|
26
rockyliang 237 天前
使用`go run -race`命令检测这段代码,确实是存在竞态并发安全问题,具体是在`b[i] = charset[_seededRand.Intn(l)]`这一行代码处
|
27
sztink 237 天前
靠,你这生成随机字符代码难怪看着眼熟,我前段时候从网络上 copy 的一份跟你的几乎一模一样,我得赶紧 fix 一下。copy 来源: https://github.com/BelphegorPrime/lib/blob/master/RandString.go
|
28
hopingtop 237 天前 3
这里能展示一下, 真正写入 全是 0 ,上层的函数吗?
比如这个函数上面有 recover , 这个时候你们得到的 值可能是 空字符串, 但是在你们存入的时候, 这个空字符串经过一些 编码,比如 hex 这种, 就会变成 0000..000 但是是 64 位, 假设你存入的 varchar(32) 在不经过 mysql 严格模式下, 就能把 空字符串 变成 32 位 000... 然后误解,是这里生成的了。 当前上面也只是我的一种 推理可能性 |
29
0o0O0o0O0o 237 天前
-race
|
30
NoobPhper 237 天前
想象一下同一 ns 情况下 两个 routine 调用了 你哪个不安全的 var , 得到的结果
|
32
nuk 237 天前
遇到过差不多的问题,不过因为代码不多,很快就发现是 race condition
https://imgur.com/dp2lnEy |
33
hopingtop 237 天前
@hopingtop #28 现在肯定能够确认这段代码非线程安全的,会有数据竞争,出现 Panic
但是就是特别想知道为啥是 32 个 0 看是否是我上面描述的猜测 |
36
mightybruce 237 天前 1
回答竟然都是竞态并发安全问题, 你们真的是认真的吗
|
37
sztink 237 天前
@afxcn 这样是解决不了本质问题的。因为 rand.NewSource 不是并发安全的。另外 createRand 中_seededRand 初始化的逻辑也不是并发安全的。你应该直接用全局函数 rand.Intn()。
|
38
cloudzhou 237 天前
@mightybruce 这就是最大可能阿,难道不是并发问题?起码代码就不是并发安全
你是认真的吗? |
39
kuanat 237 天前 1
|
40
kneo 237 天前 via Android
你的意思是现在重现不了了,2021 年能重现?是我理解错了吗?几年前的事情你现在才来问?
|
41
afxcn OP @kneo 确实是几年前遇到的问题,当时解决起来也容易,换种写法就可以了,只是到目前为止还是没搞清楚是因为什么,所以来问了。
说是返回 32 个 0 可能也不是绝对准确的,也可能只是注意到了全是 0 的 token ,因为它最明显。 |
42
jiayiming001 237 天前
func main() {
var wait sync.WaitGroup for i := 0; i < 100; i++ { wait.Add(1) go func() { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() defer wait.Done() for j := 0; j < 100; j++ { helper.RandString(10) } }() } wait.Wait() } 运行这个程序,可以看到有的 goroutine 出现了 painc |
45
zzhaolei 237 天前
1. 现在的 go1.22.2 版本,并发调用 Intn 并不会 panic ,源码里就没有相关的检查
2. 当 go 协程调用 _seededRand 的时候,程序的运行时已经将 _seededRand 初始化完成,并不存在说你加了 if _seededRand == nil {...init...} 就会好的情况 3. 几年前的问题了,你也不知道当时具体是什么 go 版本,现有的数据不足以用来判断 |
46
Ipsum 237 天前
rand.NewSource 记得会有读写过程,你多个协程并发会有 data race
|
47
afxcn OP |
48
yeyypp92 237 天前
rand 不是线程安全的
|
51
MrSeven7 236 天前
亲测并发会 panic ,楼主说的返回 32 个 0 可能是自己的业务代码做了默认值处理吧
|
53
zzhaolei 236 天前
https://go.dev/play/p/GaXBGyvGkEn 我试出来了,确实会 panic ,Source 的 Uint64 方法内部会索引一个定长的数组,并发的情况下可能会出现索引越界的情况
https://imgur.com/QsyRcDU |
54
bv 236 天前
|
55
AceGo 236 天前
@zzhaolei rand 中 Source 的注释:
// A Source represents a source of uniformly-distributed // pseudo-random int64 values in the range [0, 1<<63). // // A Source is not safe for concurrent use by multiple goroutines. NewSource 方法的 // NewSource returns a new pseudo-random Source seeded with the given value. // Unlike the default Source used by top-level functions, this source is not // safe for concurrent use by multiple goroutines. // The returned Source implements Source64. |
56
afxcn OP |