单元测试NUnit,mock组件NSubstitute,信号量SemaphoreSlim,异步lock等例子
public class LockTest { private IDatabase _database; private readonly Random _random = new Random(); private int _num = 0; [SetUp] public void SetUp() { _num = 0; //信号量,同时访问的线程1 var slim = new SemaphoreSlim(1, 1); //mock redis的接口 _database = Substitute.For<IDatabase>(); //mock LockTake 同时只有一个线程可以拿到锁 //slim.WaitAsync 参数为0时标识直接返回是否拿到锁,不会类似lock阻塞直至其他人释放 //这里主要mock redis的场景,拿不到要使用循环重复尝试拿锁,因此传0 //普通异步锁场景根据需求选择等待时间,使用等待时间理论上使用响应时间应比自己写循环简单且有更好的性能 _database.LockTakeAsync(Arg.Any<RedisKey>(), Arg.Any<RedisValue>(), Arg.Any<TimeSpan>()) .Returns(async call => await slim.WaitAsync(call.ArgAt<TimeSpan>(2))); //mock LockRelease 释放锁 _database.LockReleaseAsync(Arg.Any<RedisKey>(), Arg.Any<RedisValue>()).Returns(call => { slim.Release(); return true; }); } /// <summary> /// 使用锁的版本,_num值会按预期加至5 /// </summary> /// <returns></returns> [Test] public async Task WithLockTest() { var tasks = Enumerable.Range(1, 5).Select(c => LockTestTaskInner("1", "1", TimeSpan.FromSeconds(0))); await Task.WhenAll(tasks); Assert.AreEqual(_num, 5); } /// <summary> /// 无锁版本,_num值会配其他线程覆盖 /// </summary> /// <returns></returns> [Test] public async Task WithoutLockTestTask() { var tasks = Enumerable.Range(1, 5).Select(async c => { var tmp = _num; //模拟覆盖写 await Task.Delay(_random.Next(5)); _num = tmp + 1; }); await Task.WhenAll(tasks); Console.WriteLine(_num); Assert.AreNotEqual(_num, 5); } private async Task LockTestTaskInner(string key, string value, TimeSpan expiry) { var reTryGetLock = true; while (reTryGetLock) { var taskLockSuccess = await _database.LockTakeAsync(key, value, expiry); if (taskLockSuccess) { reTryGetLock = false; try { var tmp = _num; //模拟覆盖写 await Task.Delay(_random.Next(5)); _num = tmp + 1; } finally { await _database.LockReleaseAsync(key, value); } } else { //证明LockTakeAsync不会阻塞 //Console.WriteLine("retry"); await Task.Delay(_random.Next(10)); } } }