views:

323

answers:

3

Can I use an Interlocked.* synchronization method to update a DateTime variable?

I wish to maintain a last-touch time stamp in memory. Multiple http threads will update the last touch DateTime variable.

I appreciate that DateTime variables are value types that are replaced rather than updated.

The best I can come up with is to hold the timestamp as total ticks in a long

class x
{
  long _lastHit;

  void Touch()
  {
    Interlocked.Exchange( ref _lastHit, DateTime.Now.Ticks );   
  }
}
+1  A: 

Yes, you can do this. Your biggest problem may be that DateTime.Ticks only has a resolution of ~20 ms. So it doesn't really matter if you keep a DateTime last or a long ticks variable. But since there is no overload of Exchange for DateTime, you need to use long.

Henk Holterman
@Adam - As I was saying before your answer was removed. Thanks for mentioning the volatile keyword it was a gap in my knowledge. However from the docs it seems volatile only provides threadsafe reads but not updates.
camelCase
@Henk - 20ms is ok in this case. The use-case for the touch timestamp is a multi tenant web system. At 2am in the morning I need to check all users hosted on a DB instance are out of the system before running a data fix script.
camelCase
If you want more accuracy with your last update, you can use the System.Diagnostics.Stopwatch.GetTimestamp methodhttp://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.gettimestamp.aspx
daub815
Or you could use another property of DateTime... see my answer.
romkyns
A: 

EDIT: based on comments below from @romkyns [Thanks]

If your code is running on a 32 bit machine. then a 64 bit long is going to be written to memory in two atomic operations, which can be interrupted by a context switch. So in general you do need to deal with this issue.

But to be clear, for this specific scenario, (writing a long value which represents time ticks) it could be argued that the problem is so very unlilely as to be not worth dealing with... since (except for a split second once every 2^32 ticks), the value in the high word (32 bits) will be the same for any two concurrent writes anyway... and even in the very unlikely event that there are two concurrent writes which span that boundary, which concurrently interrupt each other and you get the hi word from one and the low word from the other, unless you are also reading this value every millesecond, the next write will fix the issue anyway, and no harm would be done. Taking this approach, however, no matter how unlikely the bad case might be, still allows for the extremely slim but possible scenario of gettign a wrong value in there once in every 4 Billion ticks... (And good luck trying to reproduce that bug...)

If you are running on a 64 bit machine, otoh, (much more likely at this point in time but not guaranteed) then the value in the 64 bit memory slot is written atomically, and you don't need to worry about concurrency here. A race condition (Which is what you are trying to prevent) can only occur if there is some program invariant that is in an invalid state during some block of processing that can be interrupted by another thread. If all you are doing is writing to the lastTouch DateTime variable (memory location) then there is no such invlaid invariant to be concerned with, and therefore you do not need to worry about concurrent access.

Charles Bretana
You are right about concurrency and logic but writing a `long` is not atomic so you _do_ need Lock or Interlocked.
Henk Holterman
@Charles - As Henk says I am concerned about reading a partially updated long value. A highly unlikely situation I appreciate.
camelCase
Henk Holterman
But the CLR Specification states "A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size is atomic when all the write accesses to a location are the same size." So unless you're still on a 32 bit machine, running 32 bit Windows, writing to a long IS atomic, no ? ---- Granted, if you aren't sure that your app will always be executed in a 64 bit hardware/OS environment, I understand taking the caustious approach
Charles Bretana
I think you should make it VERY clear that your answer only holds if nothing ever reads the value AND that you're positive you're on a native 64-bit platform. Otherwise it's highly misleading. I seriously doubt you can assume native 64-bitness for at least another 10 years, likely more (think of all those netbooks...)
romkyns
Edited to include your comments, Thanks!
Charles Bretana
A: 

Option 1: use a long with Interlocked and DateTime.ToBinary(). This doesn't need volatile (in fact you'd get a warning if you had it) because Interlocked already ensures an atomic update. You get the exact value of the DateTime this way.

long _lastHit;

void Touch()
{
    Interlocked.Exchange(ref _lastHit, DateTime.Now.ToBinary());
}

To read this atomically:

DateTime GetLastHit()
{
    long lastHit = Interlocked.CompareExchange(ref _lastHit, 0, 0);
    return DateTime.FromBinary(lastHit);
}

This returns the value of _lastHit, and if it was 0 swaps it with 0 (i.e. does nothing other than read the value atomically).

Simply reading is no good - at a minimum because the variable isn't marked as volatile, so subsequent reads may just reuse a cached value. Combining volatile & Interlocked would possibly work here (I'm not entirely sure, but I think an interlocked write cannot be seen in an inconsistent state even by another core doing a non-interlocked read). But if you do this you'll get a warning and a code smell for combining two different techniques.

Option 2: use a lock. Less desirable in this situation because the Interlocked approach is more performant in this case. But you can store the correct type, and it's marginally clearer:

DateTime _lastHit;
object _lock = new object();

void Touch()
{
    lock (_lock)
        _lastHit = DateTime.Now;
}

You must use a lock to read this value too! Incidentally, besides mutual exclusion a lock also ensures that cached values can't be seen and reads/writes can't be reordered.

Non-option: do nothing (just write the value), whether you mark it as volatile or not. This is wrong - even if you never read the value, your writes on a 32 bit machine may interleave in such an unlucky way that you get a corrupted value:

Thread1: writes dword 1 of value 1
Thread2: writes dword 1 of value 2
Thread2: writes dword 2 of value 2
Thread1: writes dword 2 of value 1

Result: dword 1 is for value 2, while dword 2 is for value 1
romkyns