代码之家  ›  专栏  ›  技术社区  ›  Damovisa

随机数在不同进程中与同一.NET代码冲突

  •  2
  • Damovisa  · 技术社区  · 15 年前

    在我开始之前,我想指出我很确定这是真的。我所有的日志都表明是这样的。

    我想知道我是否错了,这是不可能的,这是难以置信的不可能(我怀疑),或者如果不太可能,我正在做一些根本错误的事情。

    我在同一台服务器上运行了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对这个随机类进行了种子设定,我认为它可以在线程之间提供保护—一个tick是100纳秒,当然两个线程不能获得相同的种子值。

    显然,我没有说明的是,我们不是在谈论线程,而是在讨论多核处理器上的进程。这意味着这个代码可以 字面 同时跑两次。我想这就是造成冲突的原因。

    两个以大约15秒的间隔运行相同代码的进程设法在100纳秒内命中相同的代码。这有可能吗?我走对了吗?

    我很感激你的想法或建议。


    为了澄清这一点,我不能真正使用guid——我与之通信的外部进程需要一个18位数字。它是旧的,不幸的是我不能改变它。

    6 回复  |  直到 15 年前
        1
  •  5
  •   tvanfosson    15 年前

    你不想要 随机的 为此目的,您需要 独特的 数字。我和JP在一起。我认为您应该考虑使用guid作为消息ID。

    编辑 :如果您不能使用guid,那么请考虑一种方法来获得一个唯一的64位数字,并使用它连续的3位块作为8个字符字母表的索引(丢弃未使用的高位)。实现这一点的一种方法是创建一个数据库,在其中为每个新消息创建一个条目,并使用一个自动递增的64位整数作为键。使用该键并将其转换为18个字符的消息ID。

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

    毫无疑问,如果您的情况不符合上述情况之一,您还可以采用其他许多方法来实现这一点。

        2
  •  7
  •   JP Alioto    15 年前

    除非有什么原因你不能,否则你应该考虑使用 GUID 为此目的。这样可以消除碰撞。

    每条注释:您可以使用一个guid和一个64位 FNV hash 使用 XOR-folding 把你的结果控制在59位以内。不是像guid那样的防碰撞,而是比您拥有的更好。

        3
  •  3
  •   MusiGenesis    15 年前

    尝试对种子使用此函数,而不是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个进程的进行,实际上很可能其中两个进程最终会使用与随机函数完全相同的种子。

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

    如果你想担心大规模的不可能发生的事件,买彩票。

        4
  •  3
  •   rIPPER    15 年前

    另一种方法是不要使用随机类,因为它充满了这样的问题。您可以使用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
  •  1
  •   Simeon Pilgrim    15 年前

    是的,它可以发生,所以它确实发生了。

    启动时,您只应随机初始化一次。如果有许多线程同时启动,请获取datetime.now.ticks的副本,并将其传递给每个具有已知偏移量的线程,以防止在同一时间进行初始化。

        6
  •  0
  •   Ray    15 年前

    我也同意guid的想法。

    对于您的原始问题,由于记号是长的,因此此语句:

    (int)DateTime.Now.Ticks
    

    将导致溢出。不知道会发生什么样的污秽…