C# 如何对多线程场景中只应执行一次的代码进行单元测试?

C# 如何对多线程场景中只应执行一次的代码进行单元测试?,c#,.net,multithreading,unit-testing,parallel-processing,C#,.net,Multithreading,Unit Testing,Parallel Processing,类包含只应创建一次的属性。创建过程是通过一个作为传入参数的Func。这是缓存场景的一部分 测试需要注意的是,无论有多少线程试图访问该元素,创建都只发生一次 单元测试的机制是围绕访问器启动大量线程,并计算创建函数被调用的次数 这根本不是确定性的,不能保证这是有效地测试多线程访问。也许一次只有一个线程会碰到锁。(实际上,getFunctionExecuteCount在7到9之间,如果锁不存在……在我的机器上,没有任何东西保证CI服务器上的锁是相同的) 如何以确定性的方式重写单元测试?如何确保多线程多

类包含只应创建一次的属性。创建过程是通过一个作为传入参数的
Func
。这是缓存场景的一部分

测试需要注意的是,无论有多少线程试图访问该元素,创建都只发生一次

单元测试的机制是围绕访问器启动大量线程,并计算创建函数被调用的次数

这根本不是确定性的,不能保证这是有效地测试多线程访问。也许一次只有一个线程会碰到锁。(实际上,
getFunctionExecuteCount
在7到9之间,如果
锁不存在……在我的机器上,没有任何东西保证CI服务器上的锁是相同的)

如何以确定性的方式重写单元测试?如何确保多线程多次触发

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);
        }
    }
}

我认为不存在真正确定的方法,但你可以提高概率,这样就很难不引起并发竞争:

Interlocked.Increment(ref getFunctionExecuteCount);
Thread.Yield();
Thread.Sleep(1);
Thread.Yield();
return "Hello World!";

通过提高
Sleep()
参数(到10?),不发生并发竞争的可能性越来越小。

除了pid的答案之外:
这段代码实际上并没有创建很多线程

// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
    cache.Get(creator);
});
它将启动
~Environment.ProcessorCount
线程

// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
    cache.Get(creator);
});
如果你想得到很多线程,你应该明确地这样做

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
属性在测试中包含此约束

要使其具有确定性,您只需要两个线程,并确保其中一个线程阻塞函数内部,而另一个线程也尝试进入函数内部

[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);
}

现在,测试将失败。

如果将创建者设置为“慢速”(让线程休眠一段时间),则会增加其他线程命中锁并必须等待慢速线程离开锁的可能性。是否尝试单元测试线程安全性?我觉得这很奇怪。在我看来,您应该对公开的类的功能进行单元测试,其中线程安全问题更像是代码审查的主题。如果有人来删除代码的
锁定部分,而代码审查没有抓住它,我不想让单元测试捕捉到它。为什么不简单地使用
Lazy
类呢?它似乎正是你想要完成的@link:yes
Lazy
解决这个小示例中的问题。我的实际案例更复杂,涉及到更高级的案例,在这些案例中,我不能使用
Lazy
类?处理器与此有什么关系?其想法是卸载正在运行的线程,以便其他并发线程可以运行并请求锁。Yield允许中止当前时间片,以便循环可以立即交换另一个时间片。Sleep将在处理器之间执行相同的操作。啊,好的。很高兴知道!谢谢。Hi@Lucas,这个方法看起来很有希望,但是如果我注释
锁代码
,然后在一秒钟后启动第二个线程:
var t2=Task.Factory.StartNew(()=>{thread.Sleep(1000);return cache.Get(creator);})太棒了,这正是我想要的,线程操作和线程状态。非常感谢。
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);
}
var t2 = new Thread(() =>
{
    var sw = Stopwatch.StartNew();
    while (sw.ElapsedMilliseconds < 1000)
        Thread.Yield();

    r2 = cache.Get(creator);
});