【问题标题】:Random number clashes with same .Net code in different processes随机数在不同进程中与相同的 .Net 代码冲突
【发布时间】:2009-07-16 00:00:17
【问题描述】:

在开始之前,我想指出我很确定这确实发生了。我所有的日志都表明确实如此。

我想知道我是不是错了,这是不可能的,是不是非常不可能(我怀疑),或者是不是不太可能,我做的事情根本上是错误的。强>

我有 4 个相同代码的实例作为 Windows 服务在同一台服务器上运行。此服务器有一个多核 (4) 处理器。

以下是代码摘要:

public class MyProcess
{
    private System.Timers.Timer timer;

    // execution starts here
    public void EntryPoint()
    {
        timer = new System.Timers.Timer(15000);  // 15 seconds
        timer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed);
        timer.AutoReset = false;

        Timer_Elapsed(this, null);
    }

    private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        string uid = GetUID();

        // this bit of code sends a message to an external process.
        //  It uses the uid as an identifier - these shouldn't clash!
        CommunicationClass.SendMessage(uid);

        timer.Start();
    }

    // returns an 18 digit number as a string
    private string GetUID()
    {
        string rndString = "";
        Random rnd = new Random((int)DateTime.Now.Ticks);
        for (int i = 0; i < 18; i++)
        {
            rndString += rnd.Next(0, 10);
        }
        return rndString;
    }

接收这些消息的外部进程感到困惑 - 我认为是因为相同的 uid 来自两个独立的进程。基于此,GetUID() 方法似乎为两个单独的进程返回了相同的“随机”18 位字符串。

我使用 DateTime.Now.Ticks 为 Random 类播种,我认为这会在线程之间提供保护 - 一个滴答声是 100 纳秒,当然两个线程无法获得相同的种子值。

我显然没有考虑到我们不是在谈论线程,而是在谈论多核处理器上的进程。这意味着这段代码可以字面上同时运行两次。我认为这就是造成冲突的原因。

以大约 15 秒的间隔运行相同代码的两个进程设法在 100 纳秒内命中相同的代码。这可能吗?我在正确的轨道上吗?

非常感谢您的想法或建议。


为了澄清,我不能真正使用 GUID - 我正在与之通信的外部进程需要一个 18 位数字。太旧了,可惜改不了了。

【问题讨论】:

    标签: c# .net multithreading random multicore


    【解决方案1】:

    除非出于某种原因您不能这样做,否则您应该考虑为此目的使用 GUID。您将通过这种方式消除碰撞。

    每条评论:您可以使用 GUID 和 64 位 FNV hash 并使用 XOR-folding 使您的结果在您拥有的 59 位范围内。不像 GUID 那样防碰撞,但比您拥有的更好。

    【讨论】:

    • 困难在于我正在与之交谈的旧流程需要一个 18 位数字。我会使用 GUID,但我不能。
    • 感谢您的更新。我认为这个故事的寓意是当你的意思是独特时不要使用随机......
    • 我认为道德是“不要在多线程或多进程场景中为 Random 提供基于时间的种子”。
    【解决方案2】:

    您不希望 随机 数字用于此目的,您需要 唯一 数字。我和@JP 在一起。我认为您应该考虑使用 GUID 作为您的消息 ID。

    编辑:如果你不能使用 GUID,那么想办法获得一个唯一的 64 位数字,并使用它的连续 3 位块作为索引8 个字符的字母表(扔掉未使用的高位)。一种方法是建立一个数据库,您可以在其中为每个新消息创建一个条目,并使用自动递增的 64 位整数作为键。使用密钥并将其转换为 18 个字符的消息 ID。

    如果您不想依赖数据库,您可以获得在特定条件下工作的东西。例如,如果消息只需要在进程的生命周期内是唯一的,那么您可以使用进程 id 作为值的 32 位,并从随机数生成器中获取剩余的 22 位。由于没有两个同时运行的进程可以具有相同的 id,因此应保证它们具有唯一的消息 id。

    如果您的情况不符合上述任何一种情况,无疑还有许多其他方法可以做到这一点。

    【讨论】:

    • 我同意。您应该使用 GUID。
    • 查看对 JP 答案的评论 - 不能使用 GUID。
    • @Damovisa -- 如果您不能使用 GUID,我在答案中添加了一些细节,说明如何执行此操作。
    • 谢谢 tvanfosson,里面有一些很好的建议。我已经更改了代码以使用种子中的进程 ID,但我可能会将其嵌入随机数本身以确保。谢谢。
    【解决方案3】:

    试着用这个函数代替 DateTime.Now.Ticks:

    public static int GetSeed()
    {
        byte[] raw = Guid.NewGuid().ToByteArray();
        int i1 = BitConverter.ToInt32(raw, 0);
        int i2 = BitConverter.ToInt32(raw, 4);
        int i3 = BitConverter.ToInt32(raw, 8);
        int i4 = BitConverter.ToInt32(raw, 12);
        long val = i1 + i2 + i3 + i4;
        while (val > int.MaxValue)
        {
            val -= int.MaxValue;
        }
        return (int)val;
    }
    

    这基本上将 Guid 转换为 int。理论上你可以得到重复,但这在宇宙中是不可能的。

    编辑:甚至只是使用:

    Guid.NewGuid().GetHashCode();
    

    另一方面,使用 DateTime.Now.Ticks 几乎可以保证在某个时间点发生冲突。在 Windows 编程中,以远远超出计时器实际精度的单位指定计时器的分辨率是很常见的(我首先使用 Visual Basic 3.0 的计时器控件遇到此问题,该控件以毫秒为单位设置,但实际上只关闭了 18 次)第二)。我不确定这一点,但我敢打赌,如果你只是运行一个循环并打印出 DateTime.Now.Ticks,你会看到这些值以 15 毫秒左右的间隔量化。因此,随着 4 个进程的进行,实际上很可能其中两个最终会为 Random 函数使用完全相同的种子。

    由于基于 Guid 的 GetSeed 函数产生重复的可能性非常小,因此理想情况下,您希望创建某种预先计算的唯一数字库。但是,由于您在这里谈论的是单独的进程,因此您必须想出某种方法来缓存所有进程都可以读取它们的值,这很麻烦。

    如果您想担心极不可能发生的事件,请购买彩票。

    【讨论】:

    • 有趣的是,我的调查(事件后)显示 DateTime.Now 在实践中的分辨率约为 15 毫秒...让您想知道他们为什么提供 Ticks...
    • 为什么 Visual Basic 使用缇而不是像素?因为比尔盖茨个人讨厌我们。
    【解决方案4】:

    实现此目的的另一种方法是不使用 Random 类,因为它充满了此类问题。您可以使用 System.Security.Cryptography 中提供的加密质量随机数生成器完成相同的功能(随机 18 位数字)。

    我已修改您的代码以使用 RNGCryptoServiceProvider 类生成 id。

    // returns an 18 digit number as a string
    private string GetUID()
    {
        string rndString = "";
        var rnd = new RNGCryptoServiceProvider();
        var data = new byte[18];
        rnd.GetBytes(data); 
        foreach(byte item in data)
        {
            rndString += Convert.ToString((int)item % 10);
        }
        return rndString;
    }
    

    【讨论】:

    • 我以前没听说过这门课 - 谢谢,它肯定很有用!
    【解决方案5】:

    是的,它可能发生,因此它确实发生了。

    您应该只在启动时初始化 Random 一次。如果您有许多线程同时启动,请获取 DateTime.Now.Ticks 的副本并将其传递给具有已知偏移量的每个线程,以防止同时初始化。

    【讨论】:

    • 虽然它们不是线程。其中每一个都在一个完全独立的进程中运行。
    【解决方案6】:

    我也同意 GUID 的想法。

    至于你原来的问题,因为 Ticks 很长,这个语句:

    (int)DateTime.Now.Ticks
    

    会导致溢出。不知道接下来会发生什么坏事……

    【讨论】:

    • 看起来将 long 转换为 int 只是环绕。它不会抛出异常,所以作为种子就可以了。
    • 没有例外。但是包裹会增加碰撞的机会,尤其是在定时器被量化的情况下。
    • 是的,没有例外,它只是包装。值空间是 int - 32 位。
    • 我怀疑值空间小于32位是因为定时器量化...
    猜你喜欢
    • 2020-02-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-23
    相关资源
    最近更新 更多