using System; using System.Threading; public class IpcReaderWriterLock : IDisposable { /** Fields **/ private const int DEFAULT_MAX_READER_COUNT = 25; private const string NAME_PREFIX = @"IpcRWL#"; private readonly int m_maxReaderCount; private Semaphore m_readerSemaphore; private EventWaitHandle m_blockReadsEvent; private Mutex m_writerMutex; private int m_writerRecursionCount; /** Constructors **/ public IpcReaderWriterLock() : this(null, DEFAULT_MAX_READER_COUNT) { } public IpcReaderWriterLock(string name) : this(name, DEFAULT_MAX_READER_COUNT) { } public IpcReaderWriterLock(int maxReaderCount) : this(null, maxReaderCount) { } public IpcReaderWriterLock(string name, int maxReaderCount) { m_maxReaderCount = maxReaderCount; string blockReadsEventName = null; string writerMutexName = null; string readerSemaphoreName = null; if (name != null) { blockReadsEventName = string.Format("{0}{1}#{2}", NAME_PREFIX, name, "RdEv"); writerMutexName = string.Format("{0}{1}#{2}", NAME_PREFIX, name, "WrMtx"); readerSemaphoreName = string.Format("{0}{1}#{2}", NAME_PREFIX, name, "RdSem"); } m_blockReadsEvent = new EventWaitHandle(true, EventResetMode.ManualReset, blockReadsEventName); m_writerMutex = new Mutex(false, writerMutexName); m_readerSemaphore = new Semaphore(maxReaderCount, maxReaderCount, readerSemaphoreName); } /** Methods **/ public void Dispose() { // Just close all of the kernel objects we opened during construction. // Note: this method is not thread-safe. If threads race with // one another to call Dispose, some nasty bugs will arise. if (m_blockReadsEvent != null) { m_blockReadsEvent.Close(); m_blockReadsEvent = null; } if (m_writerMutex != null) { m_writerMutex.Close(); m_writerMutex = null; } if (m_readerSemaphore != null) { m_readerSemaphore.Close(); m_readerSemaphore = null; } } public void EnterReadLock() { Thread.BeginCriticalRegion(); // We first wait on the read blocking event, in case a writer // has tried to acquire the lock and wants us to wait. m_blockReadsEvent.WaitOne(); // Now take '1' from the reader semaphore to count the number // of simultaneous readers inside the lock. m_readerSemaphore.WaitOne(); } public void ExitReadLock() { // Just release '1' back to the semaphore to let others know // the number of simultaneous readers just decreased. m_readerSemaphore.Release(); Thread.EndCriticalRegion(); } public void EnterWriteLock() { Thread.BeginCriticalRegion(); // We have to first ensure only one writer can get in at a time. m_writerMutex.WaitOne(); // Increment our recursion count. m_writerRecursionCount++; // For the first writer who enters, we need to block new readers // and wait for any existing readers to exit the lock. if (m_writerRecursionCount == 1) { // Next we block any new readers from entering the lock. m_blockReadsEvent.Reset(); // And lastly, we ensure that all readers have exited the lock. // We do this by acquiring the semaphore's capacity. It's // unfortunate that the Win32 APIs don't support a take-n // function for semaphores. for (int i = 0; i < m_maxReaderCount; i++) { m_readerSemaphore.WaitOne(); } } } public void ExitWriteLock() { // We have to do everything in the reverse order as we did // during acquisition. Not doing so can lead to subtle bugs, // including lost resets and deadlocks. m_writerRecursionCount--; // The last writer to release has to signal readers. if (m_writerRecursionCount == 0) { // We release the semaphore's capacity back, enabling readers // to take from it. Note that as soon as we call this, other // threads may wake up and race to acquire the semaphore. In // fact, simultaneous readers can get in, even though we still // have a writer in here! m_readerSemaphore.Release(m_maxReaderCount); // Unblock any readers that are waiting. Note: ideally we would // do this after signaling writers, so that readers can't sneak // in before the writer, but that would be more complicated: we // keep it simple for now. m_blockReadsEvent.Set(); } // And lastly release the mutex. m_writerMutex.ReleaseMutex(); Thread.EndCriticalRegion(); } } class TestProgram { const int s_wThreads = 10; const int s_rThreads = 10; const int s_multiplier = 10000; static void Main() { CounterObj c = new CounterObj(); int c_appDomains = 10; // Create a bunch of threads in separate AppDomains. Thread[] tests = new Thread[c_appDomains]; for (int i = 0; i < tests.Length; i++) { tests[i] = new Thread(delegate() { AppDomain ad = AppDomain.CreateDomain("tst#" + i); ad.DoCallBack(c.DoTest); }); tests[i].Start(); } for (int i = 0; i < tests.Length; i++) tests[i].Join(); int expectedCount = (s_wThreads * s_multiplier * c_appDomains); bool success = c.m_counter1 == expectedCount && c.m_counter1 == c.m_counter2; Console.WriteLine(success ? "Success" : "Fail"); Console.WriteLine("Expect : counter1 = {0}, counter2 = {1}", expectedCount, expectedCount); Console.WriteLine("Observed: counter1 = {0}, counter2 = {1}", c.m_counter1, c.m_counter2); /** Perf testing: * object o = new object(); System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); for (int i = 0; i < 100000; i++) { lock (o) { } } sw.Stop(); IpcReaderWriterLock rwl = new IpcReaderWriterLock(); System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch(); sw2.Start(); for (int i = 0; i < 100000; i++) { rwl.EnterWriteLock(); rwl.ExitWriteLock(); } sw2.Stop(); Console.WriteLine(sw.Elapsed); Console.WriteLine(sw2.Elapsed); Console.WriteLine((float)sw2.ElapsedTicks / sw.ElapsedTicks); */ } class CounterObj : MarshalByRefObject { internal int m_counter1; internal int m_counter2; public void DoTest() { Test(this); } } static void Test(CounterObj c) { using (IpcReaderWriterLock rwl = new IpcReaderWriterLock("Foodawg")) { // Create writer threads: Thread[] writers = new Thread[s_wThreads]; for (int i = 0; i < s_wThreads; i++) { writers[i] = new Thread(delegate() { rwl.EnterWriteLock(); // Take a snapshot of the counter. This increases the probability of // exposing a race condition. int cnt = c.m_counter1; Thread.Sleep(0); // Sleep to increase probability of context switches. // Add a fixed number to the counter, via a loop (to stretch CPU time). for (int j = 0; j < s_multiplier; j++) { cnt++; } Thread.Sleep(0); // Sleep to increase probability of context switches. // Now store the counter back to the shared variable. c.m_counter1 = cnt; // Take a snapshot of the 2nd counter. This increases the probability // of exposing a race condition. cnt = c.m_counter2; Thread.Sleep(0); // Sleep to increase probability of context switches. // Add a fixed number to the counter, via a loop (to stretch CPU time). for (int j = 0; j < s_multiplier; j++) { cnt++; } Thread.Sleep(0); // Sleep to increase probability of context switches. // Now store the counter back to the shared variable. c.m_counter2 = cnt; // Do a quick check: Both counters should be equivalent. if (c.m_counter1 != c.m_counter2) { Console.WriteLine("WRITE FAIL"); } rwl.ExitWriteLock(); }); writers[i].Start(); } // Create the reader threads: Thread[] readers = new Thread[s_rThreads]; for (int i = 0; i < s_rThreads; i++) { readers[i] = new Thread(delegate() { rwl.EnterReadLock(); // Read the shared variable and then context switch a bunch of times, // increasing the probability of exposing a race condition. int cnt1 = c.m_counter1; for (int j = 0; j < 25; j++) { Thread.Sleep(0); } // Read the shared variable and then context switch a bunch of times, // increasing the probability of exposing a race condition. int cnt2 = c.m_counter2; for (int j = 0; j < 25; j++) { Thread.Sleep(0); } // Do a quick check: Both counters should be equivalent. if (cnt1 != cnt2) { Console.WriteLine("READ FAIL"); } rwl.ExitReadLock(); }); readers[i].Start(); } for (int i = 0; i < s_wThreads; i++) writers[i].Join(); for (int i = 0; i < s_rThreads; i++) readers[i].Join(); } } }