当前位置 主页 > 网站技术 > 代码类 >

    解决Go中使用seed得到相同随机数的问题

    栏目:代码类 时间:2019-10-17 21:03

    1. 重复的随机数

    废话不多说,首先我们来看使用seed的一个很神奇的现象。

    func main() {
      for i := 0; i < 5; i++ {
      rand.Seed(time.Now().Unix())
        fmt.Println(rand.Intn(100))
      }
    }
    
    // 结果如下
    // 90
    // 90
    // 90
    // 90
    // 90

    可能不熟悉seed用法的看到这里会很疑惑,我不是都用了seed吗?为何我随机出来的数字都是一样的?不应该每次都不一样吗?

    可能会有人说是你数据的样本空间太小了,OK,我们加大样本空间到10w再试试。

    func main() {
      for i := 0; i < 5; i++ {
      rand.Seed(time.Now().Unix())
        fmt.Println(rand.Intn(100000))
      }
    }
    
    // 结果如下
    // 84077
    // 84077
    // 84077
    // 84077
    // 84077

    你会发现结果仍然是一样的。简单的推理一下我们就能知道,在上面那种情况,每次都取到相同的随机数跟我们所取的样本空间大小是无关的。那么唯一有关的就是seed。我们首先得明确seed的用途。

    2. seed的用途

    在这里就不卖关子了,先给出结论。

    上面每次得到相同随机数是因为在上面的循环中,每次操作的间隔都在毫秒级下,所以每次通过time.Now().Unix()取出来的时间戳都是同一个值,换句话说就是使用了同一个seed。

    这个其实很好验证。只需要在每次循环的时候将生成的时间戳打印出来,你就会发现每次打印出来的时间戳都是一样的。

    每次rand都会使用相同的seed来生成随机队列,这样一来在循环中使用相同seed得到的随机队列都是相同的,而生成随机数时每次都会去取同一个位置的数,所以每次取到的随机数都是相同的。

    seed 只用于决定一个确定的随机序列。不管seed多大多小,只要随机序列一确定,本身就不会再重复。除非是样本空间太小。解决方案有两种:

    在全局初始化调用一次seed即可
    每次使用纳秒级别的种子(强烈不推荐这种)

    3. 不用每次调用

    上面的解决方案建议各位不要使用第二种,给出是因为在某种情况下的确可以解决问题。比如在你的服务中使用这个seed的地方是串行的,那么每次得到的随机序列的确会不一样。

    但是如果在高并发下呢?你能够保证每次取到的还是不一样的吗?事实证明,在高并发下,即使使用UnixNano作为解决方案,同样会得到相同的时间戳,Go官方也不建议在服务中同时调用。

    Seed should not be called concurrently with any other Rand method.

    接下来会带大家了解一下代码的细节。想了解源码的可以继续读下去。

    4. 源码解析-seed

    4.1 seed

    首先来看一下seed做了什么。

    func (rng *rngSource) Seed(seed int64) {
      rng.tap = 0
      rng.feed = rngLen - rngTap
    
      seed = seed % int32max
      if seed < 0 { // 如果是负数,则强行转换为一个int32的整数
        seed += int32max
      }
      if seed == 0 { // 如果seed没有被赋值,则默认给一个值
        seed = 89482311
      }
    
      x := int32(seed)
      for i := -20; i < rngLen; i++ {
        x = seedrand(x)
        if i >= 0 {
          var u int64
          u = int64(x) << 40
          x = seedrand(x)
          u ^= int64(x) << 20
          x = seedrand(x)
          u ^= int64(x)
          u ^= rngCooked[i]
          rng.vec[i] = u
        }
      }
    }

    首先,seed赋值了两个定义好的变量,rng.tap和rng.feed。rngLen和rngTap是两个常量。我们来看一下相关的常量定义。

    const (
      rngLen  = 607
      rngTap  = 273
      rngMax  = 1 << 63
      rngMask = rngMax - 1
      int32max = (1 << 31) - 1
    )