using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace MfGames.Locking.Tests { public class LockTests { public static readonly TimeSpan ProcessTime = TimeSpan.FromMilliseconds(50); private readonly List events; private readonly ReaderWriterLockSlim locker; private readonly ITestOutputHelper output; private readonly DateTime start; public LockTests(ITestOutputHelper output) { this.output = output; this.start = DateTime.UtcNow; this.locker = new ReaderWriterLockSlim(); this.events = new List(); } private TimeSpan Elapsed => DateTime.UtcNow - this.start; [Fact] public void BasicDataGatheringWorks() { this.Report(ProcessTime, 1); this.Verify(); } [Fact] public void ReadBlocksWrite() { Task.WaitAll( Task.Run( () => this.ReportInReadLock( ProcessTime * 0, ProcessTime * 3, 1)), Task.Run( () => this.ReportInWriteLock( ProcessTime * 1, ProcessTime * 1, 2))); this.Verify(); } [Fact] public void ReadsDoNotBlockReads() { Task.WaitAll( Task.Run( () => this.ReportInReadLock( ProcessTime, ProcessTime * 5, 3)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime, 1)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime * 2, 2))); this.Verify(); } [Fact] public void ThreadedDataGatheringWorks() { Task.WaitAll( Task.Run( () => this.ReportInReadLock(ProcessTime, ProcessTime, 1))); this.Verify(); } [Fact] public void UpgradableBlocksUpgradable() { Task.WaitAll( Task.Run( () => this.ReportInUpgradableLock( ProcessTime, ProcessTime * 5, 1)), Task.Run( () => this.ReportInUpgradableLock( ProcessTime * 3, ProcessTime, 2)), Task.Run( () => this.ReportInUpgradableLock( ProcessTime * 2, ProcessTime * 2, 3))); this.Verify(); } [Fact] public void UpgradableDoesNotBlockReads() { Task.WaitAll( Task.Run( () => this.ReportInUpgradableLock( ProcessTime, ProcessTime * 5, 3)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime, 1)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime * 2, 2))); this.Verify(); } [Fact] public void WriteBlockRead() { Task.WaitAll( Task.Run( () => this.ReportInWriteLock( ProcessTime, ProcessTime * 3, 1)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime, 2))); this.Verify(); } [Fact] public void WriteBlockReads() { Task.WaitAll( Task.Run( () => this.ReportInWriteLock( ProcessTime, ProcessTime * 5, 1)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime, 2)), Task.Run( () => this.ReportInReadLock( ProcessTime * 2, ProcessTime * 2, 3))); this.Verify(); } private void AddEvent(int sequence) { // This is a monitor lock on the slim, just to give us a second lock. lock (this.locker) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => adding sequence"); this.events.Add(sequence); } } /// /// Waits a short period of time and then injects the sequence into the event list. /// private void Report( TimeSpan processTime, int sequence) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => starting process"); Thread.Sleep(processTime); this.output.WriteLine( this.Elapsed + ": " + sequence + " => finished process"); this.AddEvent(sequence); } private void ReportInReadLock( TimeSpan settleTime, TimeSpan processTime, int sequence) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => entering read lock: " + sequence); Thread.Sleep(settleTime); this.output.WriteLine( this.Elapsed + ": " + sequence + " => locking read lock: " + sequence); using (new ReadLock(this.locker)) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => starting read lock: " + sequence); this.Report(processTime, sequence); } } private void ReportInUpgradableLock( TimeSpan settleTime, TimeSpan processTime, int sequence) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => entering upgradable lock: " + sequence); Thread.Sleep(settleTime); this.output.WriteLine( this.Elapsed + ": " + sequence + " => locking upgradable lock: " + sequence); using (new UpgradableLock(this.locker)) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => starting upgradable lock: " + sequence); this.Report(processTime, sequence); } } private void ReportInWriteLock( TimeSpan settleTime, TimeSpan processTime, int sequence) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => entering write lock: " + sequence); Thread.Sleep(settleTime); this.output.WriteLine( this.Elapsed + ": " + sequence + " => locking write lock: " + sequence); using (new WriteLock(this.locker)) { this.output.WriteLine( this.Elapsed + ": " + sequence + " => starting write lock: " + sequence); this.Report(processTime, sequence); } } /// /// Verifies the sequence of events. /// private void Verify() { this.output.WriteLine( "Final Sequence: " + string.Join(", ", this.events)); for (int i = 1; i <= this.events.Count; i++) { Assert.Equal(i, this.events[i - 1]); } } } }