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.taprng.feedrngLenrngTap是两个常量。我们来看一下相关的常量定义。

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

由此可见,无论seed是否相同,这两个变量的值都不会受seed的影响。同时,seed的值会最终决定x的值,只要seed相同,则得到的x就相同。而且无论seed是否被赋值,只要检测到是零值,都会默认的赋值为89482311

接下来我们再看seedrand。

4.2 seedrand

// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
const (
A = 48271
Q = 44488
R = 3399
) hi := x / Q // 取除数
lo := x % Q // 取余数
x = A*lo - R*hi // 通过公式重新给x赋值
if x < 0 {
x += int32max // 如果x是负数,则强行转换为一个int32的正整数
}
return x
}

可以看出,只要传入的x相同,则最后输出的x一定相同。进而最后得到的随机序列rng.vec就相同。

到此我们验证我们最开始给出的结论,即只要每次传入的seed相同,则生成的随机序列就相同。验证了这个之后我们再继续验证为什么每次取到的随机序列的值都是相同的。

5. 源码解析-Intn

首先举个例子,来直观的描述上面提到的问题。

func printRandom() {
for i := 0; i < 2; i++ {
fmt.Println(rand.Intn(100))
}
} // 结果
// 81
// 87
// 81
// 87

假设printRandom是一个单独的Go文件,那么你无论run多少次,每次打印出来的随机序列都是一样的。通过阅读seed的源码我们知道,这是因为生成了相同的随机序列。那么为什么会每次都取到同样的值呢?不说废话,我们一层一层来看。

5.1 Intn

func (r *Rand) Intn(n int) int {
if n <= 0 {
panic("invalid argument to Intn")
}
if n <= 1<<31-1 {
return int(r.Int31n(int32(n)))
}
return int(r.Int63n(int64(n)))
}

可以看到,如果n小于等于0,就会直接panic。其次,会根据传入的数据类型,返回对应的类型。

虽然说这里调用分成了Int31n和Int63n,但是往下看的你会发现,其实都是调用的r.Int63(),只不过在返回64位的时候做了一个右移的操作。

// r.Int31n的调用
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) } // r.Int63n的调用
func (r *Rand) Int63() int64 { return r.src.Int63() }

5.2 Int63

先给出这个函数的相关代码。

// 返回一个非负的int64伪随机数.
func (rng *rngSource) Int63() int64 {
return int64(rng.Uint64() & rngMask)
} func (rng *rngSource) Uint64() uint64 {
rng.tap--
if rng.tap < 0 {
rng.tap += rngLen
} rng.feed--
if rng.feed < 0 {
rng.feed += rngLen
} x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}

可以看到,无论是int31还是int63,最终都会进入Uint64这个函数中。而在这两个函数中,这两个变量的值显得尤为关键。因为直接决定了最后得到的随机数,这两个变量的赋值如下。

rng.tap = 0
rng.feed = rngLen - rngTap

tap的值是常量0,而feed的值决定于rngLen和rngTap,而这两个变量的值也是一个常量。如此,每次从随机队列中取到的值都是确定的两个值的和。

到这,我们也验证了只要传入的seed相同,并且每次都调用seed方法,那么每次随机出来的值一定是相同的

6. 结论

首先评估是否需要使用seed,其次,使用seed只需要在全局调用一次即可,如果多次调用则有可能取到相同随机数。

往期文章:

相关:

  • 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)

Go中使用seed得到相同随机数的问题的更多相关文章

  1. c语言中实现从0-1的随机数输出

    原文:c语言中实现从0-1的随机数输出 今天晚上同学问了一个巨简单的问题,问我怎么用c语言输出0-1的随机数,可别说,一时之间还想不出来.在写的过程中发现,直接调用random函数还不能实现,用以下方 ...

  2. 18.翻译系列:EF 6 Code-First 中的Seed Data(种子数据或原始测试数据)【EF 6 Code-First系列】

    原文链接:https://www.entityframeworktutorial.net/code-first/seed-database-in-code-first.aspx EF 6 Code-F ...

  3. Random 中的Seed

    C#中使用随机数 看下例 当Random的种子是0时 生成的随机数列表是一样的 也就是说当seed 一样时 审查的随机数时一样的 Random的无参实例默认 种子 时当前时间 如果要确保生成的随机数不 ...

  4. java中从1000万个随机数中查找出相同的10万个随机数花的最少时间

    偶然在群里看到有人问到大数据查询,自己也就想了小艾改如何解决,从从1000万个随机数中查找出相同的10万个随机数花的最少时间, 谈到效率,自然是hashmap莫属. import java.util. ...

  5. Java中在特定区间产生随机数

    生成指定范围内的随机数 这个是最常用的技术之一.程序员希望通过随机数的方式来处理众多的业务逻辑,测试过程中也希望通过随机数的方式生成包含大量数字的测试用例.问题往往类似于: 如何随机生成 1~100 ...

  6. Java Random、ThreadLocalRandom、UUID类中的方法应用(随机数)

    1.Random:产生一个伪随机数(通过相同的种子,产生的随机数是相同的): Random r=new Random(); System.out.println(r.nextBoolean()); S ...

  7. JAVA中获取不重复的随机数

    我们知道 Random random = new  Random() 中可能会获取到重复的随机数 那么假设要获取1到33之间的六个不重复随机数应该怎么做呢? 首先定义一个数字数组存储1到33 int[ ...

  8. Spring Boot 配置文件中使用变量、使用随机数

    参数引用 在application.properties中的各个参数之间可以直接通过是使用placeHolder的方式进行引用,如: book.author=Clark book.name=C++ b ...

  9. Java开发中经典的小实例-(随机数)

    import java.util.Random;//输出小于33的7个不相同的随机数public class probability {    static Random random = new R ...

随机推荐

  1. 2019牛客暑期多校训练营(第二场) - J - Go on Strike! - 前缀和预处理

    题目链接:https://ac.nowcoder.com/acm/contest/882/C 来自:山东大学FST_stay_night的的题解,加入一些注释帮助理解神仙代码. 好像题解被套了一次又一 ...

  2. P3705 [SDOI2017]新生舞会 分数规划 费用流

    #include <algorithm> #include <iterator> #include <iostream> #include <cstring& ...

  3. HDU2222Keywords Search AC_自动机

    http://blog.csdn.net/niushuai666/article/details/7002823 #include <iostream> #include <cstd ...

  4. HDU 5126 stars 4维偏序, CDQ套CDQ

    题目传送门 题意:在一个星空中,按着时间会出现一些点,现在john想知道,在某个时间内有多少个星星是的坐标是满足条件的.(x1<=x<=x2, y1 <= y <= y2, z ...

  5. poj3666 Making the Grade(基础dp + 离散化)

    Description A straight dirt road connects two fields on FJ's farm, but it changes elevation more tha ...

  6. springboot的最简创建方式

    springboot是目前比较流行的技术栈之一,我在这里写一个springboot工程最简方式 首先开发工具是IDEA,双击打开IDEA,点击Create new Project 进入到这个页面,选择 ...

  7. Linux开机启动过程(个人理解)

    简述Linux启动过程 1)BIOS开机自检 2)MBR引导 3)启动引导程序菜单(GRUB) 4)加载内核 5)加载虚拟文件系统加载函数模块 6)启动系统进程 /sbin/init --->/ ...

  8. js-DOM ~ 04. BOM:浏览器对象模型window. 、定时器、在线用户、祝愿墙、BOM的内置方法内置对象

    multiple. select列表多选 触发事件后调用有参数的函数要先创建一个函数,然后在函数内调用执行函数 Array.from(伪数组):伪数组变为真数组 indexOf():查询字符的索引 a ...

  9. Java中String为什么是不可变的

    1.在Java中,String类是不可变类,一个不可变类是一个简单的类,并且这个的实例也不能被修改, 这个类的实例创建的时候初始化所有的信息,并且这些信息不能够被修改 2.字符串常量池 字符串常量池是 ...

  10. Spring Boot跨域解决方案

    一.什么是跨域 为保证浏览器的安全,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,这称之为同源策略,如果一个请求地址里的协议.域名.端口号都相同,就属于同源.依据浏览器同源策略,非同源脚 ...