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

12

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

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

将对对象的访问封装在自定义锁中只有在所有并发线程都知道并使用该锁时才有效。如果对象的作用域广泛,这可能不是情况。最糟糕的情况是公共类型中的静态成员。例如,想象一下如果DateTime结构体上的静态属性DateTime.Now不是线程安全的,并且两个并发调用可能导致混乱的输出或异常。通过外部加锁来解决这个问题的唯一方法可能是在调用DateTime.Now之前锁定类型本身- lock(typeof(DateTime))。这只有在所有程序员都同意这样做(这是不太可能的)的情况下才有效。此外,锁定类型会产生自己的问题。

因此,DateTime结构体上的静态成员已经被精心编程以实现线程安全。

根据微软文档,.NOWpublic static DateTime Now { get; },即只读属性。既然是只读属性,为什么还要考虑线程安全呢?两个并发调用应该能够在不干扰彼此的情况下获取当前时间吧?
编辑:很多人指出问题不太清楚。我做了一个假设,认为它应该是安全的,因为:它是只读的,并且它是时间(总是在变化)。

3
“两个同时进行的调用应该能够在互不干扰的情况下获取当前时间?” - 是的,这意味着它是线程安全的。不清楚你在问什么。请注意,“只读”并不自动意味着“线程安全”。公开只读的东西仍然可能更改内部状态。 - Jon Skeet
1
这是一个属性。访问它会运行一些代码。并没有指定具体的代码,因此它可以是任何代码,所以当然可能不是线程安全的。 - user395760
仅仅因为一个属性是只读的,并不意味着get;内部的代码必须是线程安全的; - Vlad
2
此外,请考虑一个属性可能是只读的,但访问被另一个调用改变的数据 - List.Count 就是一个例子。 - Jon Skeet
1
我认为你已经回答了自己的问题。它需要是线程安全的,因为“两个并发调用应该能够在不干扰彼此的情况下获取当前时间。”如果两个并发调用相互干扰(例如,由于竞争条件访问缓存的时区信息),那么它就不是线程安全的。 - Raymond Chen
7个回答

9
约瑟夫给出了一个例子。不是说Now需要是线程安全的,所有静态方法都需要是线程安全的。
但是,让我们看看所有静态情况。静态方法需要本质上是线程安全的,因为如果它们有任何状态,那么它就是有效的全局(因此需要是线程安全的),并且对方法/属性的任何调用者都无法使该数据变成本地,因此需要担心线程安全问题。也就是说,调用者将无法可靠地使其线程安全,因为没有其他代码可能知道此代码如何尝试使其线程安全,因此真正不能保证线程安全。
例如,假设这个虚构的DateTime.Now像这样(糟糕地)实现:
private static long ticks;
public static DateTime Now
{
  get
  {
     ticks = UnsafeMethods.GetSystemTimeAsFileTime()
     return new DateTime(ticks); 
  }
}

由于 ticks 是一个 long 类型,在32位模式下它不是原子性的。因此,对于 共享ticks 进行赋值需要进行同步。 约瑟夫表示你不能只是这样做:

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

因为任何其他代码都可以这样做:

var now = DateTime.Now;

因此,您的lock 无法使其线程安全。

对于静态方法的消费者来说,确保调用静态方法的线程安全是不可能的,因此,静态方法的编写者需要执行所有必要的步骤来使其线程安全。


1
顺便说一下,在现实世界的硬件中,我经常看到一个设计期望通过等效于 `UInt64 GetSysTime { CaptureTime(); return ((UInt64)GetCapturedMsb() << 32) | GetCapturedLsb();} 的代码来读取时间。即使硬件能够保证MSB和LSB被原子方式捕获,但如果无法与捕获过程原子地读取,这并没有帮助。 - supercat
1
一个线程安全的方法是 UInt64 GetSysTime { UInt32 l1,l2,u; do { CaptureTime(); l1=GetCapturedLsb(); u=GetCapturedMsb(); l2=GetCapturedLsb(); } while (l1 != l2); return ((UInt64)u << 32) | l1; } 即使在增量期间尝试读取值可能会产生任意组合的旧数据和新数据,该方法也可以正常工作。 - supercat
1
我的第一个例子的目的是呈现一个真实世界的情况,即两个线程都试图读取时间,但它们可能会相互干扰,尽管读取时间应该是多个线程(或主线程和中断处理程序)可以同时进行而不会相互干扰的事情。 - supercat
1
RE长而原子,是的。请参见https://dev59.com/CGgt5IYBdhLWcg3w3xXC#11745701。在那个问题中引用了C#规范的相关部分。 - Peter Ritchie
1
就此而言,一个兼容的CLI实现可能可以在32位平台上使长原子操作成为可能,但它必须执行多个CPU指令(例如,请参见http://www.plantation-productions.com/Webster/www.artofasm.com/Windows/HTML/AdvancedArithmetica2.html#1007619,其中提供了如何在32位x86汇编中乘以64位值的示例)。 - Peter Ritchie
显示剩余4条评论

5

以下是一个不具备线程安全性的 Get 方法:

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

幸运的是,优化器可能会看到这个问题并开始修复它,但不管怎样,一个线程可能会构建“FooBar”,被中断,第二个线程重新设置为“Foo”,现在第一个线程返回“Foo”。崩溃,竞态条件。
这就是为什么即使get方法也可能需要额外的工作来保证线程安全。注意使用了一个私有字段?我愿意打赌,这种情况是如此常见,以至于它启发了.Net团队将所有非静态方法和属性默认声明为非线程安全的策略。特别注意所有静态变量都是线程安全的。
这也提醒我们多线程编程之难,因为大多数的.Net语言不能明确地表明哪些是线程安全的。大多数人在编码时都是按过程进行思考的,所以当我们编写竞争条件时,这并不立刻显而易见。只有在有证据需要时才使用并行处理。
正如Kamel BRAHIM指出的那样,静态和Get(“只读”)并不保证线程安全。不可变性(‘readonly’关键字)则可以保证线程安全,无论返回的类型是字符串还是DateTime。

2
每次调用 DateTime.Now 都需要从某个共享的可变资源中获取当前时间(因为当前时间在不断变化,它并不是一个常量)。从多个线程访问共享的可变资源意味着你需要确保以安全的方式这样做。

1
那个参数似乎不太对。DateTime.Now的实现不太可能直接管理可变资源(即当前时间),更有可能的是,DateTime.Now只是该资源的使用者。它从其他地方读取当前时间(例如通过系统/Windows API调用)并将其转发给自己的调用者。棘手的部分不在于DateTime.Now必须是线程安全的(因为由于其大多数只进行转发所以应该相当简单),而在于直接管理可变资源的任何组件。 - stakx - no longer contributing
话虽如此,我并不否认 DateTime.Now 也必须是线程安全的事实。 - stakx - no longer contributing
@stakx 当然,事实上就是这种情况,但这相当于打开黑匣子。问题中的引用讨论了DateTime作为一个黑匣子,这意味着确保DateTime公开的内容是线程安全的。执行所需操作所需的同步是在其他相关资源中完成而不是在DateTime类本身中完成的事实超出了问题的范围。重要的是,DateTime公开的API是安全的,可以从多个线程中使用,这就是引用所断言的。 - Servy
你在回答中已经打开了那个黑匣子,解释了DateTime.Now需要在内部执行的操作,这也许是我一开始对它感到困惑的原因。但我同意你的观点;也许我误解了你的回答试图表达的重点。你最后一条评论的最后一句话更加清晰明了,以我个人的看法。 - stakx - no longer contributing
1
@stakx 我的回答是指出了一个概念上的事实,为了完成返回表示当前时间的对象的任务,某个地方需要跟踪当前时间,这意味着它将会被改变,并且将从多个线程中访问。无论该代码是否在 DateTime 类中或重构到其他依赖项中并不重要,对于我们作为 DateTime 的外部用户来说。重要的是,某个人需要执行该操作,并且在执行时需要确保适当的同步。 - Servy

2

保证线程安全并不总是需要进行同步。

例如:

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

没有任何特殊编码即可实现线程安全。

请记住 .NET 编码指南:静态成员应该是线程安全的(即除非另有说明),因此这是默认位置。但是,这个指南并未提及实现线程安全所需的任何步骤:可能可以零成本实现。

一个只读属性可以缓存当前值(可能需要耗费大量资源以确定,但很少更改),可能需要使用 Monitor 同步缓存,但如何实现线程安全是一种实现细节。

编辑 为了回应评论“不回答问题”:否则,DateTime.Now 就不是线程安全的了——每个程序都需要在每次调用 DateTime.Now 周围提供自己的同步。(我认为基本问题是:“指南说我应该做 X,但 X 是隐含的,我该怎么办?”答案是:“如果你免费获得合规性,那就接受它。”)


当然,这仍然没有回答问题,而且还需要解释为什么。 - Peter Ritchie
@PeterRitchie 我试过了,但可能太抽象了,我在回答这里的潜在问题。 - Richard

2

想象一下,如果Now的实现方式如下:

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

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

因此,如果您从多个线程使用此类方法,则可能会得到诸如“昨天 0:01AM”、“今天 0:01”和“今天 11:59PM”之类的结果,即使每个值本身都是线程安全的,该方法以非线程安全的方式组合了这两个值。

因此,为了让您以线程安全的方式使用此值,库的作者必须以线程安全的方式计算该值(即在周围加锁)。


0

MSDN实际上将什么标记为问题? DateTime.Now属性的实现方式依赖于“全局”状态:请查看TimeZoneInfo.s_cachedData,它从GetDateTimeNowUtcOffsetFromUtc()中访问。

以下是它可能会给你带来麻烦的方式当一个CPU核心根据时区更改的系统事件更改静态值后,另一个核心上的旧缓存行将被DateTime.Now访问,并且可能会产生不正确的时间值,与预期值相差数小时,因为静态访问未受同步对象保护。

要解决这个问题您可以依赖于DateTime.UtcNow,并将时区信息作为单独的练习进行计算,然后将它们组合在一起。但是,我担心对于最常用的情况,这可能过于复杂了。时区偏移量并不经常更改(例如,在我所在的地方每年只有两次)。

与其他语言的比较: 属性的签名并未声明可能会影响返回结果正确性的副作用。例如,在Haskell中,它们将返回DateTime的IO单子。另一个洞察力是看看C++中通常使用关键字volatile访问硬件寄存器的方式。该关键字确保CPU中存储值的高速缓存行在每次访问时都得到正确刷新。


如果你指的是当前时区,那么是的。从你的回答中并不清楚。 - Servy
2
它一开始不明显为什么时区问题会相关,这正是你的答案如果提到了这一点而不含糊其辞将受益匪浅的原因。 - Servy
与其讨论变量的缓存名称,更好的答案是从概念上解释什么是缓存以及为什么要缓存(如果需要的话可以附上代码),因为显然问题表达了对于首先需要全局存储什么的困惑。 - Servy
我觉得你太注重细节了,解释DateTime.Now的作用只会让人分心,而真正的问题是为什么静态变量需要线程安全。 - Peter Ritchie
@PeterRitchie 我很喜欢你的回答(并点了赞),但我试图提出其他答案中被忽视的方面。 - GregC
显示剩余3条评论

0

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

DateTime.Now 看起来像这样

[__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);
      }
    }

2
这与问题无关。问题是询问为什么Now的实现需要特别注意确保它是线程安全的。它甚至不是一个实例成员,因此DateTime一旦创建就是不可变的事实是无关紧要的。在这种情况下创建一个DateTime需要进行额外的工作以确保它是安全的。 - Servy
@Servy确实需要访问共享的可变资源,您能否更明确地说明这一点?泛泛而谈是没有意义的,您已经多次编辑了您的评论。 - BRAHIM Kamel
1
这似乎与Guillaume的回答结合起来就是答案,不是吗? - George Mauer
@KamelBRAHIM 这意味着您需要确保它不会发生。如果没有考虑,它不会默认起作用。 - Servy
我同意Servy的观点,你只是关注了Now的实现而忽略了为什么 - Peter Ritchie
显示剩余3条评论

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