Thread safe DateTime update using Interlocked.*

Viewed 12276

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 );   
  }
}
4 Answers

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

My approach is not one of the best, but you can use a string var to store a formatted date and then parse it back into datetime:

class x
{
  string _lastHit;

  void Touch()
  {
    Interlocked.Exchange( ref _lastHit, DateTime.Now.ToString("format your date") );   
  }
}

When you need to use this value, just parse into DateTime:

DateTime.Parse(_lastHit)

The parsing is always working, because the string is formatted using the DateTime class, but you can use TryParse to handle possible parsing errors

Related