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 SemaphoreSlims, 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.