在单元测试中确定线程安全性

21

我正在编写一个多线程应用程序,同时也在尝试着想出如何为其编写适当的单元测试。我认为这可能是另一个最好的问题。

还有一个问题,我有一个类如下所示,知道它不是线程安全的,并希望在单元测试中证明它,但无法弄清楚如何做到:

public class MyClass
{
    private List<string> MyList = new List<string>();

    public void Add(string Data)
    {
        MyList.Add(Data);  //This is not thread safe!!
    }
}

6
一般来说,您不能使用单元测试来证明代码是线程安全的或不是线程安全的。 - David Heffernan
@DavidHeffernan 嗯......救命啊! - Jon
大多数情况下,您必须通过对代码进行推理来进行此类验证。Marc所描述的测试类型非常有用,并且带来了很多好处。但是它们并不能证明任何事情。 - David Heffernan
1
@David 可以通过示例证明某些内容不是线程安全的;问题在于无法证明它不意味着它线程安全的。 - Marc Gravell
1
@David 是的,我知道你在说什么 - 但是:如果某件事情以某种方式成功运行了100万次,而在改变了一件事情(线程)之后,它失败了100万次,我们可以预期,我们改变的东西很可能暗示着问题所在。 - Marc Gravell
显示剩余3条评论
5个回答

23

证明某个东西是线程安全的很棘手 - 可能是停机问题。您可以展示产生竞态条件很容易,或者很难产生。但是不产生竞态条件并不意味着它不存在。

但是:我通常的做法(如果我有理由认为应该是线程安全的代码确实不是)是启动许多线程,等待在单个ManualResetEvent之后。到达门口的最后一个线程(使用Interlocked计数)负责打开门,以便所有线程同时进入系统(已经存在)。然后他们进行工作并检查是否有合理的退出条件。然后我重复这个过程很多次。这通常足以重现疑似的线程竞争,并显示它从“显然破损”移动到“不以显而易见的方式破损”(这与“未破损”极其不同)。

还请注意:大多数代码不必是线程安全的。


谢谢,但我写了很多基于线程的代码,并尝试让开发团队采用单元测试/TDD,以便我们的代码需要具备线程安全性。 - Jon
1
Marc,你写的听起来很有趣,但不幸的是它并不是非常清晰。你是否愿意更详细地描述一下你的过程,并可能澄清一些问题,比如“最后一个到达门口的线程(使用互锁计数)”和“打开门”? - Mike Nakis
1
@DavidHeffernan 我明白你的意思,但也许如果我们能看到一些演示此功能的代码会更有帮助。对于不太熟悉线程和阻塞的人来说,通过示例学习可能更容易理解。 - Jon
3
@Jon 我不会声称这个代码特别漂亮(我有一种非常“实用”的方法)- 但是请看这里:http://code.google.com/p/protobuf-net/source/browse/trunk/protobuf-net.unittest/Meta/ThreadRace.cs - Marc Gravell
1
让我们在聊天室里继续这个讨论 - Jon
显示剩余10条评论

11

我经常编写单元测试来证明某些代码是线程安全的。通常,我会在生产中发现bug后编写这些测试。在这种情况下,测试的目的是演示复制bug(测试失败),以及新代码修复线程问题(测试通过),然后作为未来版本的回归测试。

我编写的大多数线程安全测试都是测试线程竞争条件,但有些还测试线程死锁。

主动进行单元测试以确保代码是线程安全的略微棘手。并非因为编写单元测试更加困难,而是因为您必须进行彻底的分析,以确定(实际上是猜测)可能存在线程不安全的情况。如果您的分析正确,则应该能够编写一个测试,在使代码线程安全之前该测试将失败。

当测试线程竞争条件时,我的测试几乎总是遵循相同的模式:(这是伪代码)

bool failed = false;
int iterations = 100;

// threads interact with some object - either 
Thread thread1 = new Thread(new ThreadStart(delegate() {
   for (int i=0; i<iterations; i++) {
     doSomething(); // call unsafe code
     // check that object is not out of synch due to other thread
     if (bad()) {
       failed = true;
     }
   }
}));
Thread thread2 = new Thread(new ThreadStart(delegate() {
   for (int i=0; i<iterations; i++) {
     doSomething(); // call unsafe code
     // check that object is not out of synch due to other thread
     if (bad()) {
       failed = true;
     }
   }
}));

thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Assert.IsFalse(failed, "code was thread safe");

做连接操作如何能够正确地测试它呢?因为连接不是等待正在执行的线程完成后再执行另一个线程吗? - Jon
4
@Jon: 一旦thread1和thread2都调用了"start()",它们就开始执行。join()只是告诉主线程(运行单元测试的线程)等待直到两个线程完成后再检查结果。thread1和thread2在“竞赛”,当它们各自调用不安全代码时,在经过一些迭代后,它们将可靠地将被测试的代码置于错误状态。当join()退出时,主线程可以检查测试是否成功或失败。 - Sam Goldberg

9

我曾经遇到一个类似的问题,我们发现了线程安全问题。为了解决它,我们必须先证明它并修复它。这个任务把我带到了这个页面,但我没有找到任何真正的答案。就像上面的许多回答所解释的那样。但是,我仍然找到了一个可能有用的方法,可以帮助其他人:

public static async Task<(bool IsSuccess, Exception Error)> RunTaskInParallel(Func<Task> task, int numberOfParallelExecutions = 2)
    {
        var cancellationTokenSource = new CancellationTokenSource();
        Exception error = null;
        int tasksCompletedCount = 0;
        var result = Parallel.For(0, numberOfParallelExecutions, GetParallelLoopOptions(cancellationTokenSource),
                      async index =>
                      {
                          try
                          {
                              await task();
                          }
                          catch (Exception ex)
                          {
                              error = ex;
                              cancellationTokenSource.Cancel();
                          }
                          finally
                          {
                              tasksCompletedCount++;
                          }

                      });

        int spinWaitCount = 0;
        int maxSpinWaitCount = 100;
        while (numberOfParallelExecutions > tasksCompletedCount && error is null && spinWaitCount < maxSpinWaitCount))
        {
            await Task.Delay(TimeSpan.FromMilliseconds(100));
            spinWaitCount++;
        }

        return (error == null, error);
    }

这不是最干净的代码,也不是我们的最终结果,但逻辑保持不变。每次我们使用这段代码都能证明它会出现线程安全问题。

我们是这样使用它的:

int numberOfParallelExecutions = 2;
RunTaskInParallel(() => doSomeThingAsync(), numberOfParallelExecutions);

希望这能对某些人有所帮助。

5

线程安全并不是一件你可以可靠测试的事情,因为它本质上是非确定性的。你可以尝试在不同的线程上并行运行相同的操作几百次,并查看结果是否一致。虽然不是很理想,但总比没有好。


1
微软研究曾经有一个运行程序,可以使用C#中的自定义线程调度器识别所有碰撞点并进行测试。但是该程序已被撤下(待产品化,因此无法从微软研究中心下载)。问题在于,您需要这种类型的工具来验证所有可能的组合,有些可能需要运行数百次才能产生,因为它们只会很少地发生。 - TomTom
@TomTom 听起来很有意思。如果能够在生产中看到类似的东西将会很不错。 - spencercw
@spencercw 同意。我认为这是真正需要的! - Jon
是的,不幸的是它需要大量处理器资源 - 好吧,我们将看到这个加上pex/moles是否很快出现在Visual Studio 2012中 ;) - TomTom

5
我已放弃使用单元测试来检测线程问题。相反,我使用一个系统负载测试,配备完整的硬件(如果有硬件——通常我的工作是在网络上的微控制器),并运行大量自动化客户端。我让它运行一周,期间我会做其他事情。一周后,我停止负载并检查剩下的内容。如果它仍然正常工作,没有对象泄漏,并且内存没有明显增加,我就发布它。这就是我所能承受的最高质量水平 :)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接