簡體   English   中英

如何在MultiThread場景中對僅應執行一次的代碼進行單元測試?

[英]How to unit test code that should execute only once in a MultiThread scenario?

類包含應該只創建一次的屬性。 創建過程是通過Func<T>傳遞的。 這是緩存方案的一部分。

測試時要注意,無論有多少線程嘗試訪問該元素,創建只會發生一次。

單元測試的機制是在訪問器周圍啟動大量線程,並計算調用創建函數的次數。

這根本不是確定性的,沒有任何保證可以有效地測試多線程訪問。 也許一次只有一個線程可以鎖定。 (實際上,如果沒有lockgetFunctionExecuteCount介於7和9之間......在我的機器上,沒有任何保證在CI服務器上它將是相同的)

如何以確定的方式重寫單元測試? 如何確保多個線程多次觸發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);
        }
    }
}

為了使它具有確定性,你只需要兩個線程並確保其中一個在函數內部阻塞,而另一個也試圖進入內部。

[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.SleepThread.Join ,請求鎖定的結果 - 例如,通過調用Monitor.EnterMonitor.Wait - 或等待線程同步對象(如ManualResetEvent

而且我們將無法判斷線程是否在休眠或等待您的ManualResetEvent ...

但是你可以在忙碌的等待中輕松替換睡眠。 注釋掉lock ,並將t2更改為:

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

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

現在測試將失敗。

我不認為存在一種確定性的方式,但你可以提高概率,這樣就很難不引起並發比賽:

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線程。 更多細節。

如果你想獲得很多線程,你應該明確地做。

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屬性在測試中包含此約束。 更多細節。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM