我正在读Joe的Albahari C#threading tutorial:
作者解释了为什么DateTime.Now
需要线程安全:
对自定义锁定周围的对象进行包装访问仅适用于所有操作 并发线程知道 - 并使用 - 锁。这可能不是 如果对象是广泛范围的情况。最糟糕的情况是静态 公共类型的成员。例如,想象一下静态属性 在DateTime结构上,DateTime.Now,不是线程安全的,那 两个并发调用可能导致输出乱码或异常。 使用外部锁定解决此问题的唯一方法可能是锁定 类型本身 - 锁(typeof(DateTime)) - 在调用DateTime.Now之前。 只有当所有程序员都同意这样做时(这是 不太可能)。此外,锁定类型会产生自己的问题。
出于这个原因,DateTime结构上的静态成员已经存在 精心编程为线程安全。
根据MS文档,.NOW
是public static DateTime Now { get; }
,即只读属性。
如果它是只读的,为什么要打扰线程安全呢?两个并发呼叫应该能够获得当前时间而不会相互干扰?
编辑:很多人指出问题不是很清楚。 我确实假设它应该是安全的,因为:它是只读的,因为它是时间(总是在变化)。
答案 0 :(得分:8)
约瑟夫举个例子。并不是 Now
需要是线程安全的,所有静态方法都需要是线程安全的。
但是,让我们看一下所有静态场景。静态需要本质上是线程安全的,因为如果它们具有任何状态,它实际上是全局(因此需要是线程安全的并且方法/属性的任何调用者都无法将该数据设置为本地,因此不需要担心线程安全。即调用者无法使其可靠地保证线程安全,因为 no 其他代码可能知道此代码如何使其成为线程安全,因此实际上不能是线程安全。
例如,假设这个虚构的DateTime.Now
已经实现(很差),就像这样:
private static long ticks;
public static DateTime Now
{
get
{
ticks = UnsafeMethods.GetSystemTimeAsFileTime()
return new DateTime(ticks);
}
}
...因为ticks
是long
,它在32位模式下不会是原子的。因此,需要同步 shared ticks
的分配。约瑟夫说你不能简单地这样做:
lock(somelock)
{
var now = DateTime.Now;
}
...因为任何其他代码都可以自由执行此操作:
var now = DateTime.Now;
...因此您的lock
不执行任何操作以使其成为线程安全的。
静态方法的使用者不可能确保对静态调用的线程安全性,因此onus的编写者需要执行所有必要的步骤以使其成为线程安全的。
答案 1 :(得分:5)
这是一个不是线程安全的Get:
private string whyWouldYouDoThis;
public string NotThreadSafe
{
get
{
whyWouldYouDoThis = "Foo";
whyWouldYouDoThis += "Bar";
return whyWouldYouDoThis;
}
}
值得庆幸的是,优化器可能会看到这一点并思考"什么......"并为你解决这个问题,但是,一个线程可以构建" FooBar",被打断,第二个线程重置为" Foo"现在第一个线程返回" Foo"。繁荣,种族状况。
这就是为什么即使获得可能还需要额外的工作来保证线程安全。注意使用私有字段?我愿意打赌这种情况非常普遍,以至于它激发了.Net的团队政策,默认情况下将所有非静态方法和属性声明为非线程安全。特别注意使所有静态线程安全。
这也是一个重要的提醒,多线程很难,因为大多数.Net语言都不能明确什么是线程安全的。我们大多数人在编码时都会在程序上进行思考,因此当我们编写竞争条件时,它并不会立即显现出来。只有在您有需要的证据时才使用并行性。
正如Kamel BRAHIM指出的那样,静态和Get("只读")不保证线程安全。不变性(' readonly'关键字)确实如此,无论返回的类型是什么,无论是字符串还是日期时间都是如此。
答案 2 :(得分:2)
线程安全并不总是需要任何同步。
例如
public static int One {
get {
return 1;
}
}
是线程安全的,没有任何特殊编码。
记住.NET编码指南:静态成员应该是线程安全的(即除非另有说明),因此这是默认位置。 但是这条指南没有说明实现这一目标所需的任何步骤:它可以是零努力。
缓存当前值的只读属性(可能确定代价很高,但变化不大)可能需要同步缓存,可能需要使用Monitor
,但是如何实现线程安全性是实施细节。
编辑 要解决评论“未解决问题”:因为否则 - DateTime.Now
不是线程安全的 - 每个程序都需要提供每次调用DateTime.Now
时都有自己的同步。 (我认为基本问题是“指南说我应该做X,但X是隐含的,我该怎么办?”答案是:“如果你得到免费的合规,那么接受它。”)。 p>
答案 3 :(得分:2)
对DateTime.Now
的每次调用都需要从一些常见的可变资源中获取当前时间(当前时间正在改变;它不像它的常量)。从多个线程访问常见的可变资源意味着您需要确保以安全的方式执行此操作。
答案 4 :(得分:2)
想象一下Now
是否实现如下:
public static DateTime Now { get { return internalToday + internalCurrentTime; } }
我们并没有声明它是线程安全的 - 这意味着"当在单线程环境中使用时,此方法只能 。
因此,如果你从多个线程中使用这样的方法,就有可能得到像"昨天0:01 AM","今天0:01"和"今天晚上11:59"因为方法以非线程安全的方式组合了两个值(即使每个都是独立的线程安全的),也是如此。
因此,为了让你以线程安全的方式使用这样的值,库的作者必须以线程安全的方式处理计算值(即锁定)。
答案 5 :(得分:0)
Date.Now
是线程安全的,因为每次需要从属性中获取值时,都会创建new
DateTime
,并且假定所有属性都在{{1}中创建并且所有属性仅constructor
使其成为get
。简单来说就是thread safe
DateTime.Now看起来像这样
immutable
答案 6 :(得分:0)
MSDN实际标记为什么问题? DateTime.Now属性的实现方式取决于“全局”状态:查看TimeZoneInfo.s_cachedData,可以从GetDateTimeNowUtcOffsetFromUtc()访问它。
以下是它如何让您陷入麻烦在一个CPU核心更改静态值以响应时区更改的系统事件后,DateTime将访问另一个核心上的陈旧缓存行。现在并且会产生一个不正确的时间值,可能会与预期值相差数小时,因为静态访问不受同步对象的保护。
解决问题 你可以依赖DateTime.UtcNow,并将时区信息作为一个单独的练习,然后将它们粉碎在一起。但是,我担心对于最常用的情况,这将是矫枉过正的。时区偏移不会经常改变(例如,我居住的地方每年只有两次)。
与其他语言的比较: 该属性的签名并未声明存在可能影响返回结果正确性的副作用。例如,在Haskell中,它们将返回DateTime的IO monad,而不是DateTime。作为另一种见解,请查看使用关键字volatile在C ++中通常访问硬件寄存器的方式。该关键字确保在每次访问时正确刷新存储该值的CPU中的缓存行。