Skip to content

Commit

Permalink
SeqLock fast optimistic rw locker.
Browse files Browse the repository at this point in the history
  • Loading branch information
Bobris committed Mar 31, 2024
1 parent d5a89f8 commit aa38910
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 3 deletions.
99 changes: 99 additions & 0 deletions BTDB/Locks/SeqLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Threading;

namespace BTDB.Locks;

/// <summary>
/// Super optimistic reader writer lock.
/// It is suitable for rare writes and many parallel reads.
/// Read cannot hang or crash or corrupt write when write is in progress.
/// Readers never block writer. Writer blocks other writers. Writer lock is not reentrant.
/// It is struct and its size is just 4 bytes.
/// Pattern for read is this:
/// var seqCounter = seqLock.StartRead();
/// retry:
/// try
/// {
/// // read data
/// if (seqLock.RetryRead(ref seqCounter)) goto retry;
/// }
/// catch
/// {
/// if (seqLock.RetryRead(ref seqCounter)) goto retry;
/// throw;
/// }
///
/// Pattern for write is this:
/// seqLock.StartWrite();
/// try
/// {
/// // write data
/// }
/// finally
/// {
/// seqLock.EndWrite();
/// }
/// </summary>
public struct SeqLock
{
uint _counter;

public uint StartRead()
{
SpinWait spin = default;
var res = _counter;
Interlocked.MemoryBarrier();
while ((res & 1u) != 0)
{
spin.SpinOnce();
res = _counter;
Interlocked.MemoryBarrier();
}

return res;
}

public bool RetryRead(ref uint seqCounter)
{
Interlocked.MemoryBarrier();
var current = _counter;
if (seqCounter != current)
{
SpinWait spin = default;
while ((current & 1u) != 0)
{
spin.SpinOnce();
current = _counter;
}

Interlocked.MemoryBarrier();
seqCounter = current;
return true;
}

return false;
}

public void StartWrite()
{
SpinWait spin = default;
while (true)
{
var counter = _counter;
while ((counter & 1u) != 0)
{
spin.SpinOnce();
counter = _counter;
}

if (Interlocked.CompareExchange(ref _counter, counter + 1u, counter) == counter)
break;
spin.SpinOnce();
}
}

public void EndWrite()
{
Interlocked.MemoryBarrier();
_counter++;
}
}
57 changes: 57 additions & 0 deletions BTDBTest/SeqLockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Threading.Tasks;
using BTDB.Locks;
using Xunit;

namespace BTDBTest;

public class SeqLockTests
{
[Fact]
public void SimpleWriteLockWorks()
{
var data = 0;
var seqLock = new SeqLock();
seqLock.StartWrite();
data++;
seqLock.EndWrite();

Assert.Equal(1, data);
}

[Fact]
public void SimpleReadLockWorks()
{
var data = 0;
var seqLock = new SeqLock();
seqLock.StartWrite();
data++;
seqLock.EndWrite();

var seqCounter = seqLock.StartRead();
Assert.Equal(1, data);
Assert.False(seqLock.RetryRead(ref seqCounter));
}

[Fact]
public void ComplexReadLockWorks()
{
var data = 0;
var seqLock = new SeqLock();

Task.Run(() =>
{
seqLock.StartWrite();
data++;
seqLock.EndWrite();
});

int readData;
do
{
var seqCounter = seqLock.StartRead();
retry:
readData = data;
if (seqLock.RetryRead(ref seqCounter)) goto retry;
} while (readData != 1);
}
}
94 changes: 94 additions & 0 deletions SimpleTester/BenchLockTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BTDB.KVDBLayer;
using BTDB.Locks;

namespace SimpleTester;

// | Method | Mean | Error | StdDev |
// |--------------------------- |----------:|----------:|----------:|
// | SeqLock_Read | 1.239 ns | 0.0044 ns | 0.0039 ns |
// | ReaderWriterLockSlim_Read | 12.508 ns | 0.0304 ns | 0.0284 ns |
// | MonitorLock_Read | 8.579 ns | 0.0291 ns | 0.0272 ns |
// | SeqLock_Write | 4.148 ns | 0.0252 ns | 0.0236 ns |
// | ReaderWriterLockSlim_Write | 13.532 ns | 0.0323 ns | 0.0302 ns |
// | MonitorLock_Write | 8.145 ns | 0.0272 ns | 0.0254 ns |

[InProcess]
public class BenchLockTest
{
ulong _global;
int _lockCounter;
ReaderWriterLockSlim _lock = new();
SeqLock _seqLock;
object _lockObj = new();

[GlobalSetup]
public void GlobalSetup()
{
Interlocked.Increment(ref _lockCounter);
_global = (ulong)Random.Shared.Next();
Interlocked.Increment(ref _lockCounter);
}

[Benchmark]
public ulong SeqLock_Read()
{
var seqCounter = _seqLock.StartRead();
retry:
var res = _global;
if (_seqLock.RetryRead(ref seqCounter)) goto retry;
return res;
}

[Benchmark]
public ulong ReaderWriterLockSlim_Read()
{
using (_lock.ReadLock())
{
return _global;
}
}

[Benchmark]
public ulong MonitorLock_Read()
{
lock (_lockObj)
{
return _global;
}
}

[Benchmark]
public void SeqLock_Write()
{
_seqLock.StartWrite();
try
{
_global++;
}
finally
{
_seqLock.EndWrite();
}
}

[Benchmark]
public void ReaderWriterLockSlim_Write()
{
using (_lock.WriteLock())
{
_global++;
}
}

[Benchmark]
public void MonitorLock_Write()
{
lock (_lockObj)
{
_global++;
}
}
}
6 changes: 3 additions & 3 deletions SimpleTester/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

//using JetBrains.Profiler.Windows.Api;
//using JetBrains.Profiler.Windows.Api;

using System;
using System.Threading;
Expand Down Expand Up @@ -48,6 +47,7 @@ static void Main(string[] args)
//new BenchTestSpanReaderWriter().GlobalSetup();
//BenchmarkRunner.Run<BenchTest>();
//new InKeyValueStressTest().Run();
BigBonTest.Run();
//BigBonTest.Run();
BenchmarkRunner.Run<BenchLockTest>();
}
}

0 comments on commit aa38910

Please sign in to comment.