Manually instrumenting C# code to catch race conditions at runtime

Generally speaking, a class that’s accessed concurrently should be made thread-safe. But what if you have a class that you know isn’t thread-safe, and you want to check whether it’s being used in a racy way by existing code? That problem in its full generality might not have a succinct solution, but here’s a way that suffices to check dynamically whether the methods of a class are being called concurrently.

class ThreadUnsafeFoo {
  // How can we make sure these methods
  // aren’t being invoked concurrently
  // with each other or with themselves?
  public int Bar() {
    /* ... statements ... */
  }
  public long Baz() {
   /* ... statements ... */
  }
}

Proposed method

Fairly minimal changes here

class ThreadUnsafeFoo {
  private readonly object locker = new();
  public int Bar()
    => AssertNoConcurrency(locker, () => { /* ... statements ... */ });
  public long Baz()
    => AssertNoConcurrency(locker, () => { /* ... statements ... */ });
}

And add the utility function1

public static T AssertNoConcurrency<T>(object locker, Func<T> function)
{
  if (!Monitor.TryEnter(locker))
  {
    Assert.Fail("Concurrent access detected");
  }
  try
  {
    return function();
  }
  finally
  {
    Monitor.Exit(locker);
  }
}

Now, for each given instance of ThreadUnsafeFoo, calling either of its member functions concurrently will fail in the assertion.

Example

Here’s a slightly more real example.

class ThreadUnsafeThing {
  private readonly object locker = new();
  public void DoSomething(int durationMs)
    => AssertNoConcurrency(locker, () => {
      // Imagine this is some thread-unsafe operation
      Thread.Sleep(durationMs);
    });
}

static void Main() {
  var thing = new ThreadUnsafeThing();
  var task = Task.Delay(250).ContinueWith(_ => { thing.DoSomething(1); });
  thing.DoSomething(500);
  task.Wait();
}

In the above code, thing.DoSomething(1) will throw, because it will occur during (about halfway through) the call tothing.DoSomething(500).

Conclusion

This is a rather obvious invention, but it can be a good utility for unit testing thread-unsafe code or trying to find the source of concurrency errors.

  1. Of course, until C# lets you use void as a type argument, you’ll also want to add an overload that takes Action instead of Func<T>. ↩︎


Posted

in

by

Tags: