Cross-process read-write synchronization primitive in .NET?
Asked Answered
S

6

9

Is there a read/write locking mechanism that works across processes (similar to Mutex, but read/write instead exclusive locking)? I would like to allow concurrent read access, but exclusive write access.

Shine answered 17/8, 2010 at 15:0 Comment(2)
Cross process or cross thread?Malvoisie
@Bipul - no but one post suggested rolling my own solution using System.Threading.Mutex, because there isn't a cross thread reader-writer lock built into the OS.Shine
S
5

No. As Richard noted above, there is no such out of the box mechanism in .NET. This is how to implement it using a mutex and a semaphore.

Method #1 is described in http://www.joecheng.com/blog/entries/Writinganinter-processRea.html, quoting:

// create or open global mutex
GlobalMutex mutex = new GlobalMutex("IdOfProtectedResource.Mutex");
// create or open global semaphore
int MoreThanMaxNumberOfReadersEver = 100;

GlobalSemaphore semaphore = new GlobalSemaphore("IdOfProtectedResource.Semaphore", MoreThanMaxNumberOfReadersEver);

public void AcquireReadLock()
{
  mutex.Acquire();
  semaphore.Acquire();
  mutex.Release();
}

public void ReleaseReadLock()
{
  semaphore.Release();
}

public void AcquireWriteLock()
{
  mutex.Acquire();
  for (int i = 0; i < MoreThanMaxNumberOfReadersEver; i++)
    semaphore.Acquire(); // drain out all readers-in-progress
  mutex.Release();
}

public void ReleaseWriteLock()
{
  for (int i = 0; i < MoreThanMaxNumberOfReadersEver; i++)
    semaphore.Release();
}

An alternative would be:

Read locking - as above. Write locking as follows (pseudocode):

- Lock mutex
- Busy loop until the samaphore is not taken AT ALL:
-- wait, release.
-- Release returns value; 
-- if value N-1 then break loop.
-- yield (give up CPU cycle) by using Sleep(1) or alternative
- Do write
- Release mutex

It must be noted that more efficient approach is possible, as here: http://en.wikipedia.org/wiki/Readers-writers_problem#The_second_readers-writers_problem Look for the words "This solution is suboptimal" in the article above.

Staciastacie answered 26/2, 2013 at 15:56 Comment(0)
J
6

Windows does not include a cross process Reader-Writer lock. A combination of Semaphore and Mutex could be used to construct ones (the Mutex is held by a writer for exclusive access or by a Reader which then uses the Semaphore to release other readers—i.e. writers would wait on just the mutex and readers for either).

However, if contention is expected to be low (i.e. no thread holds a lock for long) then mutual exclusion may still be faster: the additional complexity of the reader-writer lock overwhelms any benefit of allowing multiple readers in. (A reader-writer lock will only be faster if there are many more readers and locks are held for significant time—but only your profiling can confirm this.)

Jobe answered 17/8, 2010 at 15:55 Comment(0)
S
5

No. As Richard noted above, there is no such out of the box mechanism in .NET. This is how to implement it using a mutex and a semaphore.

Method #1 is described in http://www.joecheng.com/blog/entries/Writinganinter-processRea.html, quoting:

// create or open global mutex
GlobalMutex mutex = new GlobalMutex("IdOfProtectedResource.Mutex");
// create or open global semaphore
int MoreThanMaxNumberOfReadersEver = 100;

GlobalSemaphore semaphore = new GlobalSemaphore("IdOfProtectedResource.Semaphore", MoreThanMaxNumberOfReadersEver);

public void AcquireReadLock()
{
  mutex.Acquire();
  semaphore.Acquire();
  mutex.Release();
}

public void ReleaseReadLock()
{
  semaphore.Release();
}

public void AcquireWriteLock()
{
  mutex.Acquire();
  for (int i = 0; i < MoreThanMaxNumberOfReadersEver; i++)
    semaphore.Acquire(); // drain out all readers-in-progress
  mutex.Release();
}

public void ReleaseWriteLock()
{
  for (int i = 0; i < MoreThanMaxNumberOfReadersEver; i++)
    semaphore.Release();
}

An alternative would be:

Read locking - as above. Write locking as follows (pseudocode):

- Lock mutex
- Busy loop until the samaphore is not taken AT ALL:
-- wait, release.
-- Release returns value; 
-- if value N-1 then break loop.
-- yield (give up CPU cycle) by using Sleep(1) or alternative
- Do write
- Release mutex

It must be noted that more efficient approach is possible, as here: http://en.wikipedia.org/wiki/Readers-writers_problem#The_second_readers-writers_problem Look for the words "This solution is suboptimal" in the article above.

Staciastacie answered 26/2, 2013 at 15:56 Comment(0)
O
2

I've created this class based on Pavel's answer. I haven't tested it extensively yet, but I've created a simple winforms application to test it and so far it works well.

Please note, that it uses a semaphore, so it doesn't support reentrancy.

public class CrossProcessReaderWriterLock
{
    private readonly string _name;
    const int _maxReaders = 10;

    readonly Mutex     _mutex;
    readonly Semaphore _semaphore;

    public CrossProcessReaderWriterLock(string name)
    {
        _name = name;
        _mutex     = new Mutex(false, name + ".Mutex");
        _semaphore = new Semaphore(_maxReaders, _maxReaders, name + ".Semaphore");
    }

    public void AcquireReaderLock()
    {
        //Log.Info($"{_name} acquiring reader lock...");

        _mutex    .WaitOne();
        _semaphore.WaitOne();
        _mutex    .ReleaseMutex();

        //Log.Info($"{_name} reader lock acquired.");
    }

    public void ReleaseReaderLock()
    {
        _semaphore.Release();

        //Log.Info($"{_name} reader lock released.");
    }

    public void AcquireWriterLock()
    {
        //Log.Info($"{_name} acquiring writer lock...");

        _mutex.WaitOne();

        for (int i = 0; i < _maxReaders; i++)
            _semaphore.WaitOne(); // drain out all readers-in-progress

        _mutex.ReleaseMutex();

        //Log.Info($"{_name} writer lock acquired.");
    }

    public void ReleaseWriterLock()
    {
        for (int i = 0; i < _maxReaders; i++)
            _semaphore.Release();

        //Log.Info($"{_name} writer lock released.");
    }
}
Overripe answered 14/12, 2018 at 22:5 Comment(1)
Thanks. I used your example as the base for mine which relies only on semaphores and is async/await friendly.Chelseychelsie
C
0

If you want to avoid Writer starvation then you could consider another algorithm. I research some algorithms, which avoid the starvation of the Writer problem (e.g. in this paper). One of the solution proposal pseudo-code is the following: pseudo-code image.

public class ReadWriterSynchronizer : IDisposable
{
    public ReadWriterSynchronizer(string name, int maxReaderCount)
    {
        myIncomingOperation = new Semaphore(1, 1, name + ".Incoming");
        myReadOperation = new Semaphore(1, 1, name + ".Reader");
        myWriteOperation = new Semaphore(1, 1, name + ".Writer");
        myCrossprocessCounter = new ReaderCounter(name + ".Counter", maxReaderCount);
    }

    public void EnterReadLock()
    {
        myIncomingOperation.WaitOne();
        myReadOperation.WaitOne();

        // Local variable is necessary, because of optimalization
        int currentCount = myCrossprocessCounter.Increase();
        if (currentCount == 1)
        {
            myWriteOperation.WaitOne();
        }

        myReadOperation.Release();
        myIncomingOperation.Release();
    }

    public void ExitReadLock()
    {
        myReadOperation.WaitOne();

        // Local variable is necessary, because of optimalization
        int currentCount = myCrossprocessCounter.Decrease();
        if (currentCount == 0)
        {
            myWriteOperation.Release();
        }

        myReadOperation.Release();
    }

    public void EnterWriteLock()
    {
        myIncomingOperation.WaitOne();
        myWriteOperation.WaitOne();
    }

    public void ExitWriteLock()
    {
        myWriteOperation.Release();
        myIncomingOperation.Release();
    }

    public void Dispose()
    {
        myIncomingOperation?.Dispose();
        myReadOperation?.Dispose();
        myWriteOperation?.Dispose();
        myCrossprocessCounter?.Dispose();

        GC.SuppressFinalize(this);
    }

    private readonly ReaderCounter myCrossprocessCounter;
    private readonly Semaphore myIncomingOperation;
    private readonly Semaphore myReadOperation;
    private readonly Semaphore myWriteOperation;
}

Unfortunately, ctr variable is an integer, therefore it could only work in interprocess scenarios. I decided to replace the integer counter with a Semaphore counter (ReaderCounter) so it could be used for cross-process communication. Essentially, I used WaitOne(0) in order to decrease and Release() to increase the reader counter.

internal class ReaderCounter : IDisposable
{
    internal ReaderCounter(string name, int maxConcurrentRead)
    {
        MaximumCount = maxConcurrentRead + InitialCount;
        myReadCounterSemaphore = new Semaphore(InitialCount, MaximumCount, name);
        myIncomingOperation = new Semaphore(1, 1, name + ".Incoming");
    }

    internal int Increase()
    {
        int counter = RetrieveCurrentCount();

        // Not allowing to exceed maximum count
        if (counter != MaximumCount - 1)
        {
            counter = myReadCounterSemaphore.Release();
        }
        else
        {
            counter++;
        }

        return counter;
    }

    internal int Decrease()
    {
        int counter = RetrieveCurrentCount() - 1;
        myReadCounterSemaphore.WaitOne(0);

        return counter;
    }

    public void Dispose()
    {
        myReadCounterSemaphore?.Dispose();
        myIncomingOperation?.Dispose();

        GC.SuppressFinalize(this);
    }

    internal int MaximumCount { get; private set; }

    private const int InitialCount = 1;
    private readonly Semaphore myReadCounterSemaphore;
    private readonly Semaphore myIncomingOperation;

    private int RetrieveCurrentCount()
    {
        myReadCounterSemaphore.WaitOne(0);
        int counter = myReadCounterSemaphore.Release();
        return counter;
    }
}

NOTE: For easier usage, 1 puffer count was added to the reader counter. For example, using a 5 reader means [1,6] initial Semaphore count. Decreasing from the minimum count returns with -1 and Increasing from maximum count return with maximum count +1.

UPDATE: I have created a GitHub repository with console applications, so you can play with it. It also contains ReaderWriterSynchronizer with TryEnterReadLock() and TryEnterWriteLock() methods: https://github.com/SzilvasiPeter/Cross-process-ReaderWriterLock

Cripps answered 4/8, 2021 at 13:16 Comment(0)
M
-1

System.Threading.Mutex has a mutex that can be used for intra-process communication. If you would like functionality that it doesn't support, it can be implemented via a mutex.

Metathesize answered 17/8, 2010 at 15:5 Comment(4)
Do you know of any samples that implement a read/write mutex?Shine
This guy says it is actually not possible: #1009226Staciastacie
@PavelRadzivilovsky That question says it can't be done without using at least one kernel level object, like a mutex.Metathesize
@PavelRadzivilovsky Actually, the answer specifically states "However what you can do is use a spin lock and fall back to a mutex whenever there is contention."Metathesize
P
-4

Have you looked at System.Threading.ReaderWriteLock? Here's the MSDN Link.

Pilcher answered 17/8, 2010 at 15:5 Comment(1)
Ah. Misuderstood the question (still on first cup of coffee). I'll leave this up for a few minutes and then delete it.Pilcher

© 2022 - 2024 — McMap. All rights reserved.