为什么DateTime.Now需要线程安全

本文关键字:线程 安全 DateTime Now 为什么 | 更新日期: 2023-09-27 18:25:11

我正在阅读Joe的Albahari C#线程教程:

作者解释了为什么DateTime.Now需要线程安全:

当所有 并发线程知道并使用锁。这可能不是 对象范围广泛的情况。最坏的情况是静电 公共类型中的成员。例如,想象一下,如果静态属性 在 DateTime 结构上,DateTime.Now 不是线程安全的,并且 两个并发调用可能会导致输出乱码或异常。 通过外部锁定解决此问题的唯一方法可能是锁定 键入自身 — lock(typeof(DateTime(( — 在调用 DateTime.Now 之前。 这只有在所有程序员都同意这样做的情况下才有效(这是 不太可能(。此外,锁定类型会产生其自身的问题。

因此,DateTime 结构上的静态成员 精心编程为线程安全。

根据MS文档,.NOWpublic static DateTime Now { get; },即只读属性。如果它是只读的,为什么要为线程安全而烦恼?两个并发调用应该能够获取当前时间而不会相互干扰?

编辑:很多人,指出问题不是很清楚。我确实假设它应该是安全的,因为:它是只读的,因为它是时间(总是在变化(。

为什么DateTime.Now需要线程安全

约瑟夫举了一个例子。 并不是说Now需要线程安全,所有静态方法都需要线程安全。

但是,让我们看一下所有静态方案。 静态本质上需要是线程安全的,因为如果它们具有任何状态,它实际上是全局的(因此需要线程安全(,并且方法/属性的任何调用者都无法使该数据在本地,因此不需要担心线程安全。 即调用方将无法可靠地使其线程安全,因为没有其他代码可能知道此代码如何尝试使其线程安全,因此实际上不能线程安全。

例如,假设这个虚构的DateTime.Now是像这样实现的(很差(:

private static long ticks;
public static DateTime Now
{
  get
  {
     ticks = UnsafeMethods.GetSystemTimeAsFileTime()
     return new DateTime(ticks); 
  }
}

。因为tickslong的,所以它在32位模式下不会是原子的。 因此,需要同步对共享ticks的分配。 约瑟夫说你不能简单地这样做:

lock(somelock)
{
   var now = DateTime.Now;
}

。因为任何其他代码都可以自由执行此操作:

var now = DateTime.Now;

。因此,您的lock不会使其线程安全。

静态方法的使用者不可能确保对 static 调用的线程安全,因此 static 的编写器有责任执行所有必要的步骤以使其线程安全。

下面是一个不是线程安全的 Get:

private string whyWouldYouDoThis;
public string NotThreadSafe
{
    get
    {
        whyWouldYouDoThis = "Foo";
        whyWouldYouDoThis += "Bar";
        return whyWouldYouDoThis;
    }
}

值得庆幸的是,优化器可能会看到这一点并思考"什么......"并为您解决此问题,但按原样,一个线程可以构建"FooBar",被中断,第二个线程重置为"Foo",现在第一个线程返回"Foo"。轰隆隆,竞争条件。

这就是为什么即使是get也可能需要额外的工作才能实现线程安全。注意使用私有字段?我敢打赌,这种情况非常普遍,以至于它激发了.默认情况下,Net 将所有非静态方法和属性声明为非线程安全的团队策略。特别注意使所有静电的线程安全。

这也是一个重要的提醒,即多线程很难,因为大多数 .Net 语言并没有明确什么是线程安全的。我们大多数人在编码时都会按程序思考,因此当我们编写竞争条件时,它不会立即显现出来。仅当您有证据需要并行性时才使用并行性。

正如Kamel BRAHIM指出的那样,静态和Get("只读"(并不能保证线程安全。不可变性("只读"关键字(确实如此,无论返回的类型如何,无论是字符串还是日期时间,都是如此。

每次调用DateTime.Now都需要从一些常见的可变资源中获取当前时间(毕竟当前时间正在改变;它不像它的常量(。 从多个线程访问公共可变资源意味着您需要确保以安全的方式执行此操作。

想象一下,

如果Now实现如下:

public static DateTime Now { get { return internalToday + internalCurrentTime; } }

我们并没有声明它是线程安全的 - 这意味着"此方法只有在单线程环境中使用时才能正常工作"。

因此,如果您从多个线程使用这种方法,则可以在完全相同的时刻获得"昨天 0:01AM"、"今天 0:01"和"今天 11:59PM"等结果,因为方法以非线程安全的方式组合了 2 个值(即使每个值本身都是线程安全的(。

因此,为了让您以线程安全的方式使用这样的值,库的作者必须以线程安全的方式(即锁定(处理计算值。

线程安全并不总是需要任何同步。

例如。

public static int One {
  get {
    return 1;
  }
}

线程安全,无需任何特殊编码。

请记住 .NET 编码准则:静态成员应该是线程安全的(即,除非另有说明(,因此这是默认位置。但是,该指南没有说明实现这一目标所需的任何步骤:它可以是零努力。

缓存

当前值的只读属性(可能确定成本很高,但变化不大(可能需要同步缓存,也许使用 Monitor ,但如何实现线程安全是一个实现细节

编辑 要解决注释"不解决问题":因为否则 - DateTime.Now不是线程安全的 - 每个程序都需要围绕对DateTime.Now的每个调用提供自己的同步。(我认为潜在的问题是"指南说我应该做X,但X是隐含的,我该怎么办?"答案是:"如果你免费获得合规性,那就接受它"。

Date.Now是线程安全的,因为每次您需要从属性中获取值时,都会创建一个new DateTime,并且给定所有属性都是在constructor中创建的,并且所有属性都只是get使其thread safe。简单地说,它immutable

日期时间现在看起来像这样

[__DynamicallyInvokable]
    public static DateTime Now
    {
      [__DynamicallyInvokable] get
      {
        DateTime utcNow = DateTime.UtcNow;
        bool isAmbiguousLocalDst = false;
        long ticks1 = TimeZoneInfo.GetDateTimeNowUtcOffsetFromUtc(utcNow, out isAmbiguousLocalDst).Ticks;
        long ticks2 = utcNow.Ticks + ticks1;
        if (ticks2 > 3155378975999999999L)
          return new DateTime(3155378975999999999L, DateTimeKind.Local);
        if (ticks2 < 0L)
          return new DateTime(0L, DateTimeKind.Local);
        else
          return new DateTime(ticks2, DateTimeKind.Local, isAmbiguousLocalDst);
      }
    }

MSDN 实际上将什么标记为问题?DateTime.Now 属性的实现方式取决于"全局"状态:看看TimeZoneInfo.s_cachedData,它是从 GetDateTimeNowUtcOffsetFromUtc(( 访问的。

下面是它如何给您带来麻烦 在一个 CPU 内核更改静态值以响应时区更改的系统事件后,DateTime.Now 将访问另一个内核上的过时缓存行,并生成不正确的时间值,由于静态访问不受同步对象的保护,因此可能与预期值相差数小时。

解决问题你可以依靠DateTime.UtcNow,把时区信息作为一个单独的练习,然后把它们粉碎在一起。 但是,我担心对于最常用的情况,这将是矫枉过正的。 时区偏移量不会经常更改(例如,在我居住的地方,每年只有两次(。

与其他语言的比较:属性的签名不会声明存在可能影响返回结果正确性的副作用。 例如,在 Haskell 中,他们将返回 DateTime 的 IO monad,而不是 DateTime。 作为另一个见解,请查看使用关键字 volatile 在C++中通常访问硬件寄存器的方式。 该关键字可确保 CPU 中存储值的缓存行在每次访问时正确刷新。