为并发C#代码编写单元测试?

时间:2015-01-17 10:33:59

标签: c# multithreading unit-testing thread-safety

我一直试图解决这个问题很长一段时间了。我已经编写了一些示例代码,展示了C#中lock的用法。手动运行我的代码我可以看到它的工作方式应该如此,但当然我想编写一个单元测试来确认我的代码。

我有以下ObjectStack.cs课程:

enum ExitCode
{
    Success = 0,
    Error = 1
}

public class ObjectStack
{
    private readonly Stack<Object> _objects = new Stack<object>();
    private readonly Object _lockObject = new Object();
    private const int NumOfPopIterations = 1000;

    public ObjectStack(IEnumerable<object> objects)
    {
        foreach (var anObject in objects) {
            Push(anObject);
        }
    }

    public void Push(object anObject)
    {
        _objects.Push(anObject);
    }

    public void Pop()
    {
        _objects.Pop();
    }

    public void ThreadSafeMultiPop()
    {
        for (var i = 0; i < NumOfPopIterations; i++) {
            lock (_lockObject) {
                try {
                    Pop();
                }
                //Because of lock, the stack will be emptied safely and no exception is ever caught
                catch (InvalidOperationException) {
                    Environment.Exit((int)ExitCode.Error);
                }
                if (_objects.Count == 0) {
                    Environment.Exit((int)ExitCode.Success);
                }
            }
        }
    }

    public void ThreadUnsafeMultiPop()
    {
        for (var i = 0; i < NumOfPopIterations; i++) {
                try {
                    Pop();
                }
                //Because there is no lock, an exception is caught when popping an already empty stack
                catch (InvalidOperationException) {
                    Environment.Exit((int)ExitCode.Error);
                }
                if (_objects.Count == 0) {
                    Environment.Exit((int)ExitCode.Success);
                }
            }
    }
}

Program.cs

public class Program
{
    private const int NumOfObjects = 100;
    private const int NumOfThreads = 10000;

    public static void Main(string[] args)
    {
        var objects = new List<Object>();
        for (var i = 0; i < NumOfObjects; i++) {
            objects.Add(new object());
        }
        var objectStack = new ObjectStack(objects);
        Parallel.For(0, NumOfThreads, x => objectStack.ThreadUnsafeMultiPop());
    }
}

我正在尝试编写一个测试线程不安全方法的单元,方法是检查可执行文件的退出代码值(0 =成功,1 =错误)。

我尝试在测试中启动并运行应用程序可执行文件作为一个过程,共计100次,并在每次测试时检查退出代码值。不幸的是,它每次都是0。

非常感谢任何想法!

3 个答案:

答案 0 :(得分:5)

逻辑上,有一个非常小的代码可以解决这个问题。一旦其中一个线程进入弹出单个元素的代码块,那么pop将起作用,在这种情况下,该线程中的下一行代码将成功退出或者pop将失败,在这种情况下下一行代码将捕获异常并退出。

这意味着无论您将多少并行化放入程序,整个程序执行堆栈中仍然只有一个单点可以发生问题,而且直接在程序退出之前。

代码真的不安全,但是在任何单次执行代码时发生问题的可能性都非常低,因为它要求调度程序决定不执行将干净地退出环境的代码行,而是让一个其他线程引发异常并退出并显示错误。

证明&#34;非常困难。存在并发错误,除了非常明显的错误,因为你完全依赖于调度程序决定做什么。

查看其他一些帖子我看到这篇文章与Java有关,但引用了C#:How should I unit test threaded code?

它包含一个可能对您有用的链接:http://research.microsoft.com/en-us/projects/chess/

希望这是有用的,如果不是,请道歉。测试并发性本质上是不可预测的,就像编写示例代码一样。

答案 1 :(得分:1)

感谢所有的投入!虽然我确实认为这是由于调度程序执行等问题而难以检测到的并发问题,但我似乎找到了一个可接受的解决方案来解决我的问题。

我写了以下单元测试:

[TestMethod]
public void Executable_Process_Is_Thread_Safe()
{
   const string executablePath = "Thread.Locking.exe";
        for (var i = 0; i < 1000; i++) {
            var process = new Process() {StartInfo = {FileName = executablePath}};
            process.Start();
            process.WaitForExit();
            if (process.ExitCode == 1) {
                Assert.Fail();
            }
        }
}

当我运行单元测试时,Parallel.For中的Program.cs执行似乎有时会抛出奇怪的异常,所以我不得不将其更改为传统的for循环:

public class Program
{
    private const int NumOfObjects = 100;
    private const int NumOfThreads = 10000;

    public static void Main(string[] args)
    {
        var objects = new List<Object>();
        for (var i = 0; i < NumOfObjects; i++) {
            objects.Add(new object());
        }

        var tasks = new Task[NumOfThreads];
        var objectStack = new ObjectStack(objects);
        for (var i = 0; i < NumOfThreads; i++)
        {
            var task = new Task(objectStack.ThreadUnsafeMultiPop);
            tasks[i] = task;
        }
        for (var i = 0; i < NumOfThreads; i++)
        {
            tasks[i].Start();
        }

        //Using this seems to throw exceptions from unit test context
        //Parallel.For(0, NumOfThreads, x => objectStack.ThreadUnsafeMultiPop());
}

当然,单元测试完全依赖于您运行它的机器(快速处理器可能能够清空堆栈并在所有情况下到达临界区之前安全退出)。

答案 2 :(得分:0)

1.)您可以使用ILGenerator以Thread.Sleep(0)的形式在代码的后期构建上注入IL Inject Context开关,这很可能会帮助解决这些问题。 2.)我建议你看看微软研究团队的CHESS项目。