-
Notifications
You must be signed in to change notification settings - Fork 934
NHibernate AsyncReaderWriterLock stalls under load #2862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
So this would be a regression of #2147. Reverting to the previous design means that applications using both synchronous and asynchronous code will have bugs, because the previous design was handling the locking separately for both cases. Async operations were not locking synchronous operations, and synchronous ones were not locking async ones. The MutexSlim used for replacing the SemaphoreSlim in StackExchange.Redis seems to be an utility of some extensions having a broader purpose. It looks unlikely to me NHibernate will take a dependency on it. Until NHibernate ceases supporting anything older than .Net 5, this means we need to implement the AsyncReaderWriterLock with something else than the According to this blog, |
Nope. Fix was considered as perf improvement: dotnet/coreclr#22686 (comment) |
@maca88, any opinion on this subject, including on:
? |
I did some testing and the performance difference between .NET Framework and .NET Core is huge for the Task Parallel Library. The mentioned dotnet bug it is just a small optimization and it is not really relevant in this case. Here is what I tested: [Test]
public async Task LockHammer53()
{
var asyncReaderWriterLock = new AsyncReaderWriterLock();
var nitoAsyncReaderWriterLock = new Nito.AsyncEx.AsyncReaderWriterLock();
var semaphore = new SemaphoreSlim(1, 1);
var asyncLock = new AsyncLock();
var mutex = new Pipelines.Sockets.Unofficial.Threading.MutexSlim(int.MaxValue);
var sw = new Stopwatch();
sw.Start();
await Task.WhenAll(Enumerable.Range(1, 100).Select(x => Task.Run(() =>
{
for (var i = 0; i < 10000; i++)
{
// lock
// .NET 461 => approx. 15ms
// .NET Core => approx. 85 ms
//lock (sw)
//{
//}
// NHibernate AsyncLock
// .NET 461 => approx. 7000ms
// .NET Core => approx. 60 ms
//using (asyncLock.Lock())
//{
//}
// NHibernate AsyncReaderWriterLock
// .NET 461 => approx. 20000ms
// .NET Core => approx. 160 ms
//using (asyncReaderWriterLock.WriteLock())
//{
//}
// Nito.AsyncEx.AsyncReaderWriterLock
// .NET 461 => approx. 80000ms
// .NET Core => approx. 7000 ms
//using (nitoAsyncReaderWriterLock.WriterLock())
//{
//}
// SemaphoreSlim
// .NET 461 => approx. 6000ms
// .NET Core => approx. 60 ms
//semaphore.Wait();
//semaphore.Release();
// Pipelines.Sockets.Unofficial.MutexSlim
// .NET 461 => approx. 5000ms
// .NET Core => approx. 5000ms
//using (mutex.TryWait())
//{
//}
}
})));
Console.WriteLine(sw.ElapsedMilliseconds);
} Every class was tested separately in .NET Framework and .NET Core in Release mode. As you can see in the comments the difference between @willg1983 If I understand correctly, with NHibernate version
As you mentioned, the second option (reverting to the old locking) is not a good idea as it may not work correctly when mixing async/sync methods. Another option would be to introduce a configuration that would lock by using |
@maca88 If I got it right dotnet/runtime#28717 is about thread pool starvation. So aside from slow execution for particular jobs it slows down the whole process. So it looks related to me.. Or are you saying the modified code in fix is not involved here? |
@maca88 we moved from nHibernate |
My point is that the performance improvement that they applied for the mentioned issue is minimal and even if it is ported to .NET Framework, our issue will still remain.
Ok now I understand, after benchmarking the
|
If we go with adding setting I would add new setting with ability to provide own locker class. |
This comment has been minimized.
This comment has been minimized.
Given the speed of lock() across both .NET (core) and .NET Framework would it be feasible to use that type of lock for both? Other types of lock might be slightly faster on .NET Core (although other CPUs/systems may have different results) but not by much and the alternatives are a lot slower on .NET Framework. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Fixed by #2944. |
Not sure this is fixed for async code. We are still getting:
The 'fix' is to add in the old locker, but doesn't work in async scenarios. @willg1983 have you any work arounds? |
We don't have built-in fix for async scenario in Full .NET Framework. But now you can implement your own locker ( |
Basic implementation could be something like https://gist.github.com/bahusoid/dd91502076316bf40c597adc8a090486#file-mutexslimcachelock-cs To use it: Configuration cfg = ...
cfg.SetProperty(Environment.CacheReadWriteLockFactory, typeof(MutexSlimCacheFactory).AssemblyQualifiedName); Note: Added just for reference I didn't really test it and have no idea it if really improves performance. |
Thanks for this, really appreciate it. The timeout for obtaining a lock should be shortish I guess right? E.G 1-2 seconds? Edit: Doing some rudimentary test with NBomber, using MutexSlim locks quicker than the default lock mechanism. |
No, we reverted to 5.2 and have avoided updating to later versions. |
did you ever upgrade from 5.2? We are currently running 5.3.20 and are having similar issues as you've explained Edit: Turns out our problem was not related to this issue in the end even though it showed up as similar symptoms. If anyone else lands here and is as stumped as we was I recommend "dotnet counters monitor npgql - " and see if you have "busy connections" slowly increasing over time (https://www.npgsql.org/doc/diagnostics/metrics.html) |
We've been investigating our process stalling under load, multiple crash dumps show a similar set of stack traces to this:

After some testing I put together a crude harness over NHibernate.Util.AsyncReadWriteLock which reproduces the problem - run this on .NET Framework 4.8 and the unit test doesn't complete after an hour, switch to .NET Core 3.0 and it runs < 1 minute.
It looks like it might be related to this bug in .NET
dotnet/runtime#28717
It was fixed in .NET Core 3.0 but remains unfixed in .NET Framework and it looks like they have no plans to fix it.
Interestingly StackExchange.Redis was majorly impacted by this and worked around this issue by switching from SemaphoreSlim to MutexSlim
StackExchange/StackExchange.Redis@6873941
So I would propose either:
a) Reverting the locking to the design in 5.2
b) Rewrite the AsyncReaderWriterLock to avoid the bug in SemaphoreSlim
The text was updated successfully, but these errors were encountered: