如何在MultiThread场景中对仅应执行一次的代码进行单元测试?

时间:2023-01-13 23:30:03

A class contains an attribute that should be created only one time. The creation process is via a Func<T> which is pass in argument. This is a part of a caching scenario.

类包含应该只创建一次的属性。创建过程是通过Func 传递的。这是缓存方案的一部分。

The test take care that no matter how many threads try to access the element, the creation occurs only once.

测试时要注意,无论有多少线程尝试访问该元素,创建只会发生一次。

The mechanism of the unit test is to launch a great number of threads around the accessor, and count how many times the creation function is called.

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

This is not deterministic at all, nothing guaranteed that this is effectively testing a multithread access. Maybe there will be only one thread at a time that will hit the lock. (In reality, getFunctionExecuteCount is between 7 and 9 if the lock is not there... On my machine, nothing guaranteed that on the CI server it's going to be the same)

这根本不是确定性的,没有任何保证可以有效地测试多线程访问。也许一次只有一个线程可以锁定。 (实际上,如果没有锁定,getFunctionExecuteCount介于7和9之间......在我的机器上,没有任何保证在CI服务器上它将是相同的)

How the unit test can be rewritten in a deterministic way? How to be sure that the lock is triggered multiple times by multiple thread?

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

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

The worst scenario is if someone break the lock code, and the testing server had some lag. This test shouldn't pass:

最糟糕的情况是,如果有人破解了锁码,测试服务器有一些滞后。此测试不应通过:

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

3 个解决方案

#1


5  

To make it deterministic, you only need two threads and ensure one of them blocks inside the function, while the other tries to get inside as well.

为了使它具有确定性,你只需要两个线程并确保其中一个在函数内部阻塞,而另一个也试图进入内部。

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

You may want to set a timeout on this test :)

你可能想在这个测试上设置超时:)


Here's a variant allowing to test more cases:

这是一个允许测试更多案例的变体:

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

If you want to delay the second call, you won't be able to use Thread.Sleep, as it'll cause the thread to go to the WaitSleepJoin state:

如果你想延迟第二次调用,你将无法使用Thread.Sleep,因为它会导致线程进入WaitSleepJoin状态:

The thread is blocked. This could be the result of calling Thread.Sleep or Thread.Join, of requesting a lock — for example, by calling Monitor.Enter or Monitor.Wait — or of waiting on a thread synchronization object such as ManualResetEvent.

线程被阻止。这可能是调用Thread.Sleep或Thread.Join,请求锁定的结果 - 例如,通过调用Monitor.Enter或Monitor.Wait - 或等待线程同步对象(如ManualResetEvent)。

And we won't be able to tell if the thread is sleeping or waiting on your ManualResetEvent...

我们将无法判断线程是否正在休眠或等待您的ManualResetEvent ...

But you can easily substitute the sleep with a busy wait. Comment out the lock, and change t2 to:

但是你可以在忙碌的等待中轻松替换睡眠。注释掉锁定,并将t2更改为:

var t2 = new Thread(() =>
{
    var sw = Stopwatch.StartNew();
    while (sw.ElapsedMilliseconds < 1000)
        Thread.Yield();

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

Now the test will fail.

现在测试将失败。

#2


1  

I don't think a really deterministic way exists, but you can raise the probability so that it's very difficult to not cause concurrent races:

我不认为存在一种确定性的方法,但是你可以提高概率,这样就不会引起并发比赛:

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

By raising the Sleep() parameter (to 10?) it gets more and more improbable that no concurrent race takes place.

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

#3


1  

In addition to pid's answer:
This code doesn't actually create a lot of threads.

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

// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
    cache.Get(creator);
});

It will start ~Environment.ProcessorCount threads. More details.

它将启动~Watan.ProcessorCount线程。更多细节。

If you want to get a lot of threads you should do it explicitly.

如果你想获得很多线程,你应该明确地做。

var threads = Enumerable.Range(0, 100)
    .Select(_ => new Thread(() => cache.Get(creator))).ToList();
threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());

So if you will have enough threads and you will enforce them to switch you will get concurrent race.

因此,如果你有足够的线程并且你将强制它们进行切换,你将获得并发竞争。

If you care about case when your CI server will have only one free core, you can include this constraint in your test by changing Process.ProcessorAffinity property. More details.

如果您关心CI服务器只有一个可用核心的情况,则可以通过更改Process.ProcessorAffinity属性在测试中包含此约束。更多细节。

#1


5  

To make it deterministic, you only need two threads and ensure one of them blocks inside the function, while the other tries to get inside as well.

为了使它具有确定性,你只需要两个线程并确保其中一个在函数内部阻塞,而另一个也试图进入内部。

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

You may want to set a timeout on this test :)

你可能想在这个测试上设置超时:)


Here's a variant allowing to test more cases:

这是一个允许测试更多案例的变体:

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

If you want to delay the second call, you won't be able to use Thread.Sleep, as it'll cause the thread to go to the WaitSleepJoin state:

如果你想延迟第二次调用,你将无法使用Thread.Sleep,因为它会导致线程进入WaitSleepJoin状态:

The thread is blocked. This could be the result of calling Thread.Sleep or Thread.Join, of requesting a lock — for example, by calling Monitor.Enter or Monitor.Wait — or of waiting on a thread synchronization object such as ManualResetEvent.

线程被阻止。这可能是调用Thread.Sleep或Thread.Join,请求锁定的结果 - 例如,通过调用Monitor.Enter或Monitor.Wait - 或等待线程同步对象(如ManualResetEvent)。

And we won't be able to tell if the thread is sleeping or waiting on your ManualResetEvent...

我们将无法判断线程是否正在休眠或等待您的ManualResetEvent ...

But you can easily substitute the sleep with a busy wait. Comment out the lock, and change t2 to:

但是你可以在忙碌的等待中轻松替换睡眠。注释掉锁定,并将t2更改为:

var t2 = new Thread(() =>
{
    var sw = Stopwatch.StartNew();
    while (sw.ElapsedMilliseconds < 1000)
        Thread.Yield();

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

Now the test will fail.

现在测试将失败。

#2


1  

I don't think a really deterministic way exists, but you can raise the probability so that it's very difficult to not cause concurrent races:

我不认为存在一种确定性的方法,但是你可以提高概率,这样就不会引起并发比赛:

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

By raising the Sleep() parameter (to 10?) it gets more and more improbable that no concurrent race takes place.

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

#3


1  

In addition to pid's answer:
This code doesn't actually create a lot of threads.

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

// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
    cache.Get(creator);
});

It will start ~Environment.ProcessorCount threads. More details.

它将启动~Watan.ProcessorCount线程。更多细节。

If you want to get a lot of threads you should do it explicitly.

如果你想获得很多线程,你应该明确地做。

var threads = Enumerable.Range(0, 100)
    .Select(_ => new Thread(() => cache.Get(creator))).ToList();
threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());

So if you will have enough threads and you will enforce them to switch you will get concurrent race.

因此,如果你有足够的线程并且你将强制它们进行切换,你将获得并发竞争。

If you care about case when your CI server will have only one free core, you can include this constraint in your test by changing Process.ProcessorAffinity property. More details.

如果您关心CI服务器只有一个可用核心的情况,则可以通过更改Process.ProcessorAffinity属性在测试中包含此约束。更多细节。