使用DateTime.Now.Ticks生成唯一数字ID

19

我需要生成一个唯一的数字ID来附加到一个传入的请求。这个ID仅用于临时跟踪请求,并在请求完成处理后将被丢弃。这个ID只会在这个应用程序的上下文中使用,但需要以高性能多线程的方式分配。

我想使用DateTime.Now.Ticks来生成这个ID,但想知道如果同时处理并发请求,是否仍然会生成冲突的ID?

如果有人可以建议一个更好的方法来生成这些ID(最好不是像Tick一样的Int64),请告诉我。即使只是一个递增的数字也足够简单,只要我不必在递增之前锁定这个数字。


3
使用GUID/UUID有什么问题? - Nick Johnson
生成GUID的开销太大了。我所需要的ID仅需要在进程内是唯一的(不是全局的),并且仅在每个请求的持续时间内使用。 - shyneman
1
“太多的开销”?一个GUID只需要生成几个字节的随机数据,这并不昂贵。听起来你正在进行过早的优化。 - Nick Johnson
1
也许在实际时间上微不足道,但经过一些基本测试,它看起来比使用8个并发线程的Interlocked.Increment慢大约10倍。 - shyneman
好的,如果你产生了足够多的ID并且这很重要,那么就可以了。 - Nick Johnson
7个回答

14

你只需要使用一个静态变量,每次想要另一个唯一的值时就将其递增即可。你可以通过使用Interlocked.Increment方法使其线程安全且非常快速。

// Declaration
private static int safeInstanceCount = 0;

// Usage
{
      ...
      Interlocked.Increment(ref safeInstanceCount);
      ...
}

我已经考虑使用interlock.increment了,但是我希望避免任何类型的锁。然而,正如你所提到的,性能影响可能是可以忽略的,因此我可能会选择这个方法,而不是花费太多时间进行过度优化。谢谢。 - shyneman
除非您计划每分钟调用数百万次,否则性能影响不值得考虑。此外,它非常易于使用。 - Phil Wright
1
@PhilWright:在我的Linux机器上,time csharp <<< "int x; while (System.Threading.Interlocked.Increment(ref x)<1000000000);" 显示它大约每秒执行121,521,448次增量操作(比仅使用 x++ 慢2.6倍)。因此(像往常一样),是否需要优化实际上取决于在那一秒钟内还需要完成什么其他任务。 - sehe
1
然而,当线程化时性能会降低:ThreadStart a = () => { int x=0; while(Interlocked.Increment(ref x) < 1000000000); }; 并行x4需要9秒;共享int x(通过将其从lambda中取出)需要32秒(仅约3100万次增量/秒),而对Interlocked.Increment的总调用次数实际上减少了4倍 :) - sehe
有趣的反馈。我怀疑与 interlock.increment 相关的时间与实际操作所需的时间相比较小。 - Phil Wright
2
我认为你应该更改示例,因为问题要求提供生成 ID 的方法,而实现方式可能会出错,如果按照这种方式编写代码:Interlocked.Increment(ref safeInstanceCount); Id = safeInstanceCount;改为下面的方式:Id = Interlocked.Increment(ref safeInstanceCount);将更好地解释解决方案。 #在我看来 - MosheG

7

DateTime.Now并不适合这个目的。最好的情况下,你只能得到1毫秒的分辨率;最差的情况是在NT系统上需要17毫秒,在CE/Compact Framework上需要1秒钟!

考虑使用Interlocked.Increment方法来实现快速且线程安全的计数器。


这篇文章说:“一毫秒有10,000个滴答声。”。 - PeterX
@PeterX,“1个tick的定义=100ns”并不意味着DateTime.Now的实现具有这样的精度;我在帖子中提到了所有实现限制。如果您真的需要微秒级精度,Stopwatch做得更好。 - Jeffrey Hantin
没问题 - 是的,我已经改用 Guid 来确保我的临时文件夹 - 无论如何都会被清理干净 - 是唯一的。 - PeterX

4

如果有多个线程发起请求,首先使用每个线程的ID,再加上每个线程的计数器(如果每个线程都需要发起多个请求)。


2
只需获取一个强随机数或使用GUID。 如果高性能是必须的功能,应按单调序列分配连续编号。通过为处理消息的每个线程“保留”一定范围(如20-100)的ID来防止锁争用。这样,您只需要在20-100次迭代中锁定序列生成器一次即可避免锁竞争。

1
使用随机似乎会“随机”发生冲突。 - Kirk Woll
性能至关重要,这就是为什么我避免使用 GUID 或计算 ID 的原因。为每个线程保留一定范围的 ID 是一个不错的想法。我会再考虑一下这个问题。谢谢。 - shyneman
@KirkWoll:如果随机数足够随机,那就不会冲突。因此,广泛认为GUID可以被假定不会发生冲突。 - sehe

1

如果您知道将要使用多少线程(或至少有一个上限),则可以在线程之间分配ID空间,计算ID值为(线程本地)计数器的值和线程ID的组合 - 例如,counter_value++ << 8 | thread_id。因此,不需要在线程之间进行协调或锁定,并且生成ID仅需要增量、位移和或运算。

如果您使用系统线程ID,则ID会稍微长一些,但是您不需要手动为线程分配ID。


-1

我正在使用基于时间的简单生成ID,可能会有所帮助。

private static readonly string prefixNumber = ConfigManager.Current.GetAppSetting("AppTimeIdPrefix", "");
private static readonly DateTime epoch = DateTime.SpecifyKind(DateTime.Parse(ConfigManager.Current.GetAppSetting("AppTimeIdEpoch", "1970/01/01")), DateTimeKind.Utc);
/// <summary>
/// DateTime.Now is only updated every 10-15ms.
/// </summary>
private static long lastTime = CurrentTimeMilliseconds();
private static readonly object timeLock = new object();

private static long CurrentTimeMilliseconds()
{
   return (long)(DateTime.UtcNow.ToUniversalTime() - epoch).TotalMilliseconds;
}

public static long CreateId()
{
    lock (timeLock)
    { // prevent concurrent access to ensure uniqueness
        long original, newValue;
       do
      {
         original = lastTime;
         newValue = Math.Max(CurrentTimeMilliseconds(), original + 1);
      } while (Interlocked.CompareExchange(ref lastTime, newValue, original) != original);

        return long.Parse(string.Format("{0}{1}", prefixNumber, newValue));
     }
}

请参考如何确保时间戳始终唯一?


在受锁保护的区域内使用Interlocked类会使使用Interlocked的初衷失去意义(避免由锁引起的竞争)。 - Theodor Zoulias
没错。我完全同意您的观点。谢谢! 上述代码中已删除了锁对象。 - Colin Chen

-1

可以使用这个属性,但是需要支付1毫秒的时间,不过这并不重要!

public static long UniqId { 
    get { 
        Thread.Sleep(1); 
        return long.Parse(DateTime.Now.ToString("yyMMddHHmmssffff")); 
    } 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接