易失性的 DateTime

12

DateTime 不能被声明为 volatile,这是正确的吗?

private DateTime _time;
public DateTime Time
{
    get
    {
        Thread.MemoryBarrier();
        return _time;
    }
    set
    {
        _time = value;
        Thread.MemoryBarrier();
    }
}

该属性可能会被不同的线程访问,因此我希望确保它们始终获取最新版本,而不使用争用(lock)。

编辑:

  • 我有一组难以创建的项目,每个项目都有一个名为CreationTimeDateTime属性,指示创建该项的时间。它初始化为DateTime.UtcNow
  • 每次访问一个项目时,该属性都会更新为DateTime.UtcNow
  • 有一个线程在定时计时器中运行,检查if (DateTime.UtcNow + 1小时)> item.CreationTime,如果为真,则删除该项。

我想确保当“删除线程”进入集合时,所有项目都具有其最新的“上次访问”DateTime,因此我可以避免再次创建该项,只是因为缓存保存了该值几毫秒。 :D


1
考虑到您正在实现一个缓存,该缓存会删除早于特定时间段的未使用对象,我认为InterlockedExchange解决方案是正确的选择。 - Moo-Juice
2
你知道DateTime.Now只精确到1/64秒,对吧?如果你担心缓存延迟几毫秒导致不准确,那么你已经输了;你试图保持准确的数据的精度始终远远小于1/1000秒。 - Eric Lippert
1
@Eric,问题:DateTime.Now没有列出1/64秒。这在哪里有记录? - user47589
1
@yodaj007:确实如此。它在操作系统NT3.5及更高版本中表示,精度约为10毫秒或1/100秒。 "约"的意思是"不完全准确";操作系统可以决定适当的精度。由于明显的原因,它通常接近于线程量子。在我的机器上,它的精度为1/64秒;你的机器可能更好或更差。 - Eric Lippert
1
@Eric:明白了。我计算的是1/64=0.015秒,或15.6毫秒。但我没有看到“近似”这个词。谢谢。 - user47589
显示剩余2条评论
4个回答

18

没错。

不过,你有另一种选择。 将时间存储为Int64 tick计数,并使用InterlockedExchange进行设置。 线程可以使用Int64构造函数构建自己的DateTime,这样就没有争用和锁定。

编辑:

鉴于您提供了更多信息,现在更容易提供一个示例。

public class Cache
{
    class CacheEntry
    {
        private Int64 m_Touched;

        public CacheEntry()
        {
            Touch();
        }

        public void Touch() 
        {
            System.Threading.Interlocked.Exchange(ref m_Touched, DateTime.Now.Ticks);
        }

        public DateTime Touched
        {
            get
            {
                return new DateTime(Interlocked.Read(ref m_Touched));
            }
        }
    } // eo class CacheEntry
} // eo class Cache

5
最好使用ToBinary/FromBinary而不仅仅是存储原始的时间戳,这样可以保留DateTimeKind信息。(尽管我认为该提问者可能应该只使用lock和普通的DateTime,而不是试图过度聪明。) - LukeH
2
虽然这两个建议都是可行的,但听起来可能会涉及到很多冗余的转换。当然,这取决于DateTime值有多少个消费者。 - Jens H
2
@Sensai76,也许吧,但我怀疑转换过程会带来一些麻烦的开销(虽然不知道其他线程如何使用这个),而且说实话,相比某种形式的锁定,它的开销会更小。 - Moo-Juice
1
你正在以原子方式设置值,但没有任何保证Touched属性将以原子方式读取它。 - vgru
我认为在DateTime(Interlocked.Read(ref m_Touched))前面缺少一个新的关键字。 - Kenci

3
这是不可能的 - 您需要使用lockMonitor类来同步访问该字段。
这是因为DateTime是一个值类型 - 一个结构体。
来自MSDN - volatile (C# 参考):

可以将volatile关键字应用于以下类型的字段:

  • 引用类型。
  • 指针类型(在不安全的上下文中)。请注意,尽管指针本身可以是volatile的,但它所指向的对象不能是volatile的。换句话说,您不能声明“指向volatile的指针”。
  • 诸如sbyte、byte、short、ushort、int、uint、char、float和bool之类的类型。
  • 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int或uint。
  • 已知为引用类型的泛型类型参数。
  • IntPtr和UIntPtr。

正如其他人提到的那样,您可以使用Ticks来跟踪时间。

3

由于DateTime的赋值不能保证原子性,因此您的代码不是线程安全的。一般来说,32位整数的赋值是原子性的,但64位的则不一定。

您可能可以使用Interlocked.Exchange以存储DateTime的滴答数,因为它可以原子地存储Int64。

但如果您切换到滴答数,则需要知道仅使用了62位进行滴答数,另外2位用于种类。因此,您不会失去种类。

即使您使getter和setter成为原子和线程安全的,我也不确定这是否足够。因为时间可能会在getter返回和您实际使用获取的时间之间更改。因此,您的时间可能始终过时。


1
这是一个针对x64机器的x64应用程序。这有关系吗?好问题。 - vtortola
2
根据@vtortola的说法,64位赋值可以是原子性的:CLI保证对于值类型变量的读写操作,其大小(或更小)等于处理器自然指针大小的变量是原子性的;如果您在64位CLR的64位操作系统上运行C#代码,则64位double和long整数的读写也被保证是原子性的。虽然C#语言不保证这一点,但运行时规范确实如此。_ https://dev59.com/CGgt5IYBdhLWcg3w3xXC#11745471 - Ohad Schneider

1
.NET Core 包括 Unsafe.As 方法,可用于执行对 DateTime 变量的 volatile 读/写操作,例如:
public static DateTime VolatileRead(ref DateTime location)
{
    ref ulong unsafeLocation = ref Unsafe.As<DateTime, ulong>(ref location);
    ulong result = Volatile.Read(ref unsafeLocation);
    return Unsafe.As<ulong, DateTime>(ref result);
}

public static void VolatileWrite(ref DateTime location, DateTime value)
{
    ref ulong unsafeLocation = ref Unsafe.As<DateTime, ulong>(ref location);
    ref ulong unsafeValue = ref Unsafe.As<DateTime, ulong>(ref value);
    Volatile.Write(ref unsafeLocation, unsafeValue);
}

使用方法:

DateTime latest = VolatileRead(ref _dateTimeField);

VolatileWrite(ref _dateTimeField, newDateTime);

这是一种hack方法,因为它依赖于DateTime类型永远由ulong字段支持。您可以根据情况自行判断是否适合/安全/明智使用此方法。
从DateTime struct的源代码(链接)中。
private readonly ulong _dateData;

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