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.
- 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>. ↩︎