一个类包含一个应该只创建一次的属性。创建过程是通过参数传递的Func<T>
。这是缓存方案的一部分。
测试时要注意,无论有多少线程尝试访问该元素,创建只会发生一次。
单元测试的机制是在访问器周围启动大量线程,并计算调用创建函数的次数。
这根本不是确定性的,没有任何保证可以有效地测试多线程访问。也许一次只有一个线程可以锁定。 (实际上,如果getFunctionExecuteCount
不存在,lock
介于7和9之间......在我的机器上,没有任何保证在CI服务器上它将是相同的<) / p>
如何以确定的方式重写单元测试?如何确保多个线程多次触发lock
?
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Test
{
public class MyObject<T> where T : class
{
private readonly object _lock = new object();
private T _value = null;
public T Get(Func<T> creator)
{
if (_value == null)
{
lock (_lock)
{
if (_value == null)
{
_value = creator();
}
}
}
return _value;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
int getFunctionExecuteCount = 0;
var cache = new MyObject<string>();
Func<string> creator = () =>
{
Interlocked.Increment(ref getFunctionExecuteCount);
return "Hello World!";
};
// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
cache.Get(creator);
});
Assert.AreEqual(1, getFunctionExecuteCount);
}
}
}
最糟糕的情况是,如果有人违反lock
代码,并且测试服务器有一些延迟。这个测试不应该通过:
using NUnit.Framework;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Test
{
public class MyObject<T> where T : class
{
private readonly object _lock = new object();
private T _value = null;
public T Get(Func<T> creator)
{
if (_value == null)
{
// oups, some intern broke the code
//lock (_lock)
{
if (_value == null)
{
_value = creator();
}
}
}
return _value;
}
}
[TestFixture]
public class UnitTest1
{
[Test]
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
int getFunctionExecuteCount = 0;
var cache = new MyObject<string>();
Func<string> creator = () =>
{
Interlocked.Increment(ref getFunctionExecuteCount);
return "Hello World!";
};
Parallel.ForEach(Enumerable.Range(0, 2), threadIndex =>
{
// testing server has lag
Thread.Sleep(threadIndex * 1000);
cache.Get(creator);
});
// 1 test passed :'(
Assert.AreEqual(1, getFunctionExecuteCount);
}
}
}
答案 0 :(得分:5)
为了使它具有确定性,你只需要两个线程并确保其中一个在函数内部阻塞,而另一个也试图进入内部。
[TestMethod]
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
var evt = new ManualResetEvent(false);
int functionExecuteCount = 0;
var cache = new MyObject<object>();
Func<object> creator = () =>
{
Interlocked.Increment(ref functionExecuteCount);
evt.WaitOne();
return new object();
};
var t1 = Task.Run(() => cache.Get(creator));
var t2 = Task.Run(() => cache.Get(creator));
// Wait for one task to get inside the function
while (functionExecuteCount == 0)
Thread.Yield();
// Allow the function to finish executing
evt.Set();
// Wait for completion
Task.WaitAll(t1, t2);
Assert.AreEqual(1, functionExecuteCount);
Assert.AreEqual(t1.Result, t2.Result);
}
您可能希望在此测试中设置超时:)
这是一个允许测试更多案例的变体:
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
var evt = new ManualResetEvent(false);
int functionExecuteCount = 0;
var cache = new MyObject<object>();
Func<object> creator = () =>
{
Interlocked.Increment(ref functionExecuteCount);
evt.WaitOne();
return new object();
};
object r1 = null, r2 = null;
var t1 = new Thread(() => { r1 = cache.Get(creator); });
t1.Start();
var t2 = new Thread(() => { r2 = cache.Get(creator); });
t2.Start();
// Make sure both threads are blocked
while (t1.ThreadState != ThreadState.WaitSleepJoin)
Thread.Yield();
while (t2.ThreadState != ThreadState.WaitSleepJoin)
Thread.Yield();
// Let them continue
evt.Set();
// Wait for completion
t1.Join();
t2.Join();
Assert.AreEqual(1, functionExecuteCount);
Assert.IsNotNull(r1);
Assert.AreEqual(r1, r2);
}
如果您想延迟第二个电话,您将无法使用Thread.Sleep
,因为这会导致该线程进入WaitSleepJoin
状态:
线程已被阻止。这可能是调用
Thread.Sleep
或Thread.Join
,请求锁定的结果 - 例如,通过调用Monitor.Enter
或Monitor.Wait
- 或等待线程同步对象,如为ManualResetEvent
。
我们将无法判断线程是否正在等待ManualResetEvent
...
但是你可以在忙碌的等待中轻松替换睡眠。注释掉lock
,并将t2
更改为:
var t2 = new Thread(() =>
{
var sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < 1000)
Thread.Yield();
r2 = cache.Get(creator);
});
现在测试将失败。
答案 1 :(得分:1)
我不认为存在一种确定性的方式,但你可以提高概率,这样就很难不引起并发比赛:
Interlocked.Increment(ref getFunctionExecuteCount);
Thread.Yield();
Thread.Sleep(1);
Thread.Yield();
return "Hello World!";
通过提高Sleep()
参数(到10?),不会发生并发竞争的可能性越来越大。
答案 2 :(得分:1)
除了pid的回答:
这段代码实际上并没有创建很多线程。
// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
cache.Get(creator);
});
它将启动~Environment.ProcessorCount
个线程。 More details.
如果你想获得很多线程,你应该明确地做。
var threads = Enumerable.Range(0, 100)
.Select(_ => new Thread(() => cache.Get(creator))).ToList();
threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());
因此,如果您有足够的线程并且您将强制它们进行切换,那么您将获得并发竞争。
如果您关心CI服务器只有一个免费核心的情况,您可以通过更改Process.ProcessorAffinity
属性在测试中包含此约束。 More details.