I'm trying to wrap my head around the new System.Threading.Lock object in c#9.
In the old days I used to use good old lock(object) to prevent multiple threads accessing the same part of code like so:
public class LazyLoadedItem
{
}
private LazyLoadedItem _item = null;
private readonly object MyLock = new object();
public LazyLoadedItem GetItem
{
get
{
{
if (_item != null)
return _item;
lock (MyLock)
{
// Serve the queue of threads that passed the first check when the floodgates opened...
if (_item != null)
return _item;
_item = new LazyLoadedItem(); // get from Database/FileSystem/Web
}
return _item;
}
}
}
public void ClearCache()
{
_item = null;
}
This worked well, and I could even safely do recursive stuff in the same thread using this lock since the lock was "thread-bound".
However.. since the await async
era it was no longer safe to lock this way, since the thread would be reused as soon as it would need to wait for the database/web/filesystem or anything else. Since then I would adopt a new pattern using a SemaphoorSlim
to ensure safe locking, like so:
private readonly SemaphoreSlim MyLock = new SemaphoreSlim(1, 1);
public async Task<LazyLoadedItem> GetItem(CancellationToken requestCancelled)
{
if (_item != null)
return _item;
await MyLock.WaitAsync(requestCancelled); // throws exception when cancelled
try
{
// Serve the queue of threads that passed the first check when the floodgates opened...
if (_item != null)
return _item;
_item = await GetLazyLoadedItem().ConfigureAwait(false); // get from Database/FileSystem/Web
// note that we may be continuing on a different thread here!
}
finally
{
MyLock.Release();
}
return _item;
}
Slightly off topic, but is it safe to await MyLock.WaitAsync(requestCancelled).ConfigureAwait(false);
? I never dared to ConfigureAwait(false)
for this particular call.
Any way, my question about the new System.Threading.Lock
is where does it fit in? Is it "async safe"?
If not I guess it's use is restricted to code that never awaits anything (for example caching calculations like total quantity * weight of some collection) which in my opinion reduces it's use-case to such a small margin that the introduction in c#9 is hardly justified. The only potential application I can come up with is caching reflection like so:
private static Dictionary<string, PropertyInfo> _props = null;
private static readonly System.Threading.Lock _myLock = new System.Threading.Lock();
public Dictionary<string,PropertyInfo> Props
{
get
{
if (_props != null)
return _props;
lock (_myLock)
{
// Serve the queue of threads that passed the first check when the floodgates opened...
if (_props != null)
return _props;
_props = this.GetType().GetProperties().ToDictionary(p => p.Name);
}
return _props;
}
}
This may be useful for Serialization or Injection containers (as long as GetProperties()
and ToDictionary()
are not async), but since the lock would only be used for a fraction of the process lifetime during startup or at first-use the whole point of optimization is lost.
So why was the new System.Threading.Lock
introduced? I guess in general the vast majority of code should stick to SemaphoreSlim
? What am I missing?
There are two main reasons for introduction of the new Lock
type as far as I can see:
Clear declaration of intent - using dedicated type can make code cleaner and self-documenting.
There is a relatively small performance gain (but it can accumulate!) in some scenarios when using the new type (also check out my emphasis):
But locking a Lock can be a more efficient answer ...
As is evident from this benchmark, the syntax for using both can be identical.// dotnet run -c Release -f net9.0 --filter "*" using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args); [HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")] public class Tests { private readonly object _monitor = new(); private readonly Lock _lock = new(); private int _value; [Benchmark] public void WithMonitor() { lock (_monitor) { _value++; } } [Benchmark] public void WithLock() { lock (_lock) { _value++; } } }
Lock
, however, will generally be a tad cheaper (and in the future, as most locking shifts to use the new type, we may be able to make most objects lighter weight by not optimizing for direct locking on arbitrary objects):
Method Mean WithMonitor 14.30 ns WithLock 13.86 ns
Is it "async safe"?
No. As the docs state:
When the lock is being entered and exited in a C#
async
method, ensure that there is no await between the enter and exit. Locks are held by threads and the code following an await might run on a different thread.
See also:
System.Threading.Lock
type issue @github