The following CrossProcessLock
class should be close to what you are searching for. It's a combination of a cross-process Semaphore
and an in-process SemaphoreSlim
. The result is a non-thread-affine synchronization primitive, with a potentially blocking EnterAsync
method:
public class CrossProcessLock : IDisposable
{
private readonly Semaphore _globalSemaphore;
private readonly SemaphoreSlim _localSemaphore;
public CrossProcessLock(string name)
{
_globalSemaphore = new Semaphore(1, 1, name);
_localSemaphore = new SemaphoreSlim(1, 1);
}
public async Task EnterAsync()
{
await _localSemaphore.WaitAsync().ConfigureAwait(false);
try { _globalSemaphore.WaitOne(); }
catch { _localSemaphore.Release(); throw; }
}
public void Exit()
{
_globalSemaphore.Release();
_localSemaphore.Release();
}
public void Dispose()
{
_globalSemaphore.Dispose();
_localSemaphore.Dispose();
}
}
Usage example:
var gate = new CrossProcessLock("MyLock");
await gate.EnterAsync();
try
{
await RingTheBellsAsync();
}
finally { gate.Exit(); }
The EnterAsync
method may block in case the internal _globalSemaphore
is currently acquired by another process, and no other in-process asynchronous flow is currently waiting for the lock. If you want to ensure that the current thread will not be blocked, you'll have to offload the invocation to the ThreadPool
with the Task.Run
method:
await Task.Run(() => gate.EnterAsync());
Only one ThreadPool
thread may be blocked at maximum, per CrossProcessLock
instance.
Update: And here is a cross-process async reader-writer lock, based on ideas originated from this question: Cross-process read-write synchronization primitive in .NET?
public class CrossProcessAsyncReaderWriterLock : IDisposable
{
private readonly Semaphore _globalReader;
private readonly Semaphore _globalWriter;
private readonly SemaphoreSlim _localReader;
private readonly SemaphoreSlim _localWriter;
private readonly int _maxConcurrentReaders;
public CrossProcessAsyncReaderWriterLock(string name, int maxConcurrentReaders)
{
_globalReader = new Semaphore(
maxConcurrentReaders, maxConcurrentReaders, name + ".Reader");
_globalWriter = new Semaphore(1, 1, name + ".Writer");
_localReader = new SemaphoreSlim(1, 1);
_localWriter = new SemaphoreSlim(1, 1);
_maxConcurrentReaders = maxConcurrentReaders;
}
public async Task EnterReaderAsync()
{
await _localReader.WaitAsync().ConfigureAwait(false);
try
{
_globalWriter.WaitOne();
_globalReader.WaitOne();
_globalWriter.Release();
}
finally { _localReader.Release(); }
}
public void ExitReader()
{
_globalReader.Release();
}
public async Task EnterWriterAsync()
{
await _localWriter.WaitAsync().ConfigureAwait(false);
try
{
_globalWriter.WaitOne();
for (int i = 0; i < _maxConcurrentReaders; i++) _globalReader.WaitOne();
_globalWriter.Release();
}
finally { _localWriter.Release(); }
}
public void ExitWriter()
{
_globalReader.Release(_maxConcurrentReaders);
}
public void Dispose()
{
_globalReader.Dispose();
_globalWriter.Dispose();
_localReader.Dispose();
_localWriter.Dispose();
}
}
This one uses two local SemaphoreSlim
s, and so it can potentially block two threads at maximum, per CrossProcessAsyncReaderWriterLock
instance.
The maxConcurrentReaders
specifies how many concurrent cross-process readers are allowed. Important: all processes should be configured with the same number. Setting this value too high may result to degraded performance. Setting it too low may result to increased contention.
Usage example:
var gate = new CrossProcessAsyncReaderWriterLock("MyRWLock", 10);
await gate.EnterReaderAsync();
try
{
await ReadSomethingAsync();
}
finally { gate.ExitReader(); }
Again, if keeping the current thread non-blocked is necessary, the Task.Run
could be used:
await Task.Run(() => gate.EnterReaderAsync());
I've stress-tested this class in a multithreaded console application, with multiple instances of the app running concurrently, and it seems that its read-write invariants are well enforced in the realm of each individual process. I haven't tested it with actual system resources though.