多线程软件的测试方法

时间:2010-03-13 08:41:54

标签: c++ multithreading testing mfc

我有一块成熟的地理空间软件,最近重新编写了一些区域,以便更好地利用现代PC中的多处理器。具体来说,显示,GUI,空间搜索和主要处理都被分离出来以分离线程。该软件具有相当大的GUI自动化套件,用于功能回归,另一个较小的用于性能回归。虽然所有自动化测试都已通过,但我并不相信它们能够提供足够的覆盖范围来查找与多线程相关的竞争条件,死锁和其他恶意相关的错误。您将使用哪些技术来查看是否存在此类错误?假设有一些技术可以根除,那么你会提倡哪些技术可以根除它们?

我到目前为止所做的是在调试器下运行的应用程序上运行GUI功能自动化,这样我就可以摆脱死锁并捕获崩溃,并计划构建一个边界检查器构建并重复测试版。我还通过PC-Lint对源进行了静态分析,希望找到潜在的死锁,但没有任何有价值的结果。

应用程序是C ++,MFC,多文档/视图,每个文档都有许多线程。我正在使用的锁定机制基于一个对象,该对象包括指向CMutex的指针,该指针锁定在ctor中并在dtor中释放。我使用此对象的局部变量来根据需要锁定各种代码,并且我的互斥锁有一个超时,如果达到超时,则会触发警告。我尽可能避免锁定,尽可能使用资源副本。

你会进行哪些其他测试?

编辑:我已经在许多不同的测试和编程论坛上发布了这个问题,因为我很想知道不同的思维方式和思想流派如何解决这个问题。如果你看到它在其他地方交叉发布,那么道歉。我会在一周左右的时间内提供回复的摘要链接

6 个答案:

答案 0 :(得分:10)

一些建议:

  • 利用law of large numbers并执行测试中的操作不仅一次,而且多次。
  • 通过夸大场景来对代码进行压力测试。例如。要测试你的互斥锁类,请使用受互斥锁保护的代码:
    • 非常短而快(单指令)
    • 耗时(睡眠价值很大)
    • 包含显式上下文切换(Sleep(0))
  • 在各种不同的架构上运行测试。 (即使您的软件仅限Windows,也可以在具有和不具有超线程的单核和多核处理器上进行测试,以及各种时钟速度)
  • 尝试设计您的代码,使其大多数不会暴露于多线程问题。例如。而不是访问共享数据(需要锁定或非常精心设计的锁定避免技术),让工作线程对数据副本进行操作,并使用队列与它们进行通信。然后,您只需要测试队列类的线程安全性
  • 在系统空闲时以及从其他任务加载时运行测试(例如,我们的构建服务器经常并行运行多个构建。仅此一项就显示了系统负载时发生的许多多线程错误。)
  • 避免在超时时断言。如果这样的断言失败,您不知道代码是否被破坏或者超时是否太短。相反,使用非常慷慨的超时(只是为了确保测试最终失败)。如果要测试操作的持续时间不会超过一定时间,请测量持续时间,但不要使用超时。

答案 1 :(得分:7)

虽然我同意@rstevens的答案,因为目前无法100%确定地对线程问题进行单元测试,但我发现有些事情是有用的。

首先,无论您做什么测试,都要确保在许多不同的规格盒上运行它们。我有几个构建机器,所有不同的,多核的,单核的,快速的,慢的等等。它们的多样性是好事,不同的将会引发不同的线程问题。我经常惊讶地在我的农场添加一台新的构建机器并突然暴露出一个新的线程错误;我正在谈论在其他构建机器上运行10000次的代码中暴露出来的新bug,并且在新的机器上显示了10个中的1个...

其次,您对代码执行的大多数单元测试都不需要涉及线程。通常,线程是正交的。因此,第一步是将代码分开,以便您可以测试执行工作的实际代码,而不必过多担心线程性质。这通常意味着创建一个线程代码用来驱动实际代码的接口。然后,您可以单独测试实际代码。

你可以测试线程代码与代码主体交互的位置。这意味着为您开发的接口编写模拟以分隔两个代码块。到目前为止,线程代码可能更简单,然后您可以经常将同步对象放在您已经创建的模拟中,以便您可以控制被测试的代码。因此,您可以启动线程并等待它通过调用模拟来设置事件,然后让它阻止您的测试代码控制的另一个事件。然后,测试代码可以将线程代码从接口中的一个点转到下一个点。

最后(如果你已经解开了足够的东西,你可以做更早的事情那么这很容易)你可以运行测试的应用程序的多线程部分的更大部分,并确保你得到你的结果期望;你可以使用线程的优先级,甚至可以添加几个测试线程,只需吃CPU就可以搞砸了。

现在,您可以在不同的硬件上多次运行所有这些测试......

我还发现在DevPartner BoundsChecker之类的东西下运行测试(或应用程序)可以帮助很多,因为它与线程调度混淆,因此它有时会很难找到bug。我还写了一个死锁检测工具,它在程序执行期间检查锁定反转,但我很少使用它。

您可以在此处查看我如何测试多线程C ++代码的示例:http://www.lenholgate.com/blog/2004/05/practical-testing.html

答案 2 :(得分:2)

不是真的答案:

测试多线程错误非常困难。如果两个(或更多)线程以特定顺序转到代码中的特定位置,则大多数错误都会出现。 如果并且何时满足该条件可能取决于过程运行的时间。由于以下前提条件之一,此时间可能会发生变化:

  • 处理器类型
  • 处理器速度
  • 处理器/核心数
  • 优化级别
  • 在调试器内部或外部运行
  • 操作系统

肯定会有更多我忘记的先决条件。

因为MT-bugs如此高度依赖于运行Heisenberg的“不确定性原则”的代码的确切时间,所以在这里:如果你想测试MT错误,你可以通过“措施”改变时间,这可以防止错误发生...

时间问题是导致MT错误非常不确定的原因。 换句话说:您可能拥有一个运行数月的软件,然后在某一天崩溃,之后可能会运行多年。如果您没有一些调试日志/核心转储等,您可能永远不知道它崩溃的原因。

所以我的结论是:没有真正好的方法来进行线程安全的单元测试。编程时,你总是要睁大眼睛。

为了清楚说明,我将从现实生活中给出一个(简化的)示例(我在更换雇主并查看现有代码时遇到过这种情况):

想象一下你有一堂课。如果没有人再使用它,您希望该类自动删除。所以你在这个类中构建了一个引用计数器: (我知道在其中一个方法中删除类的实例是一种不好的方式。这是因为使用Ref类来处理计数引用的实际代码的简化。)

class A {
  private:
    int refcount;
  public:
    A() : refcount(0) {
    }
    void Ref() {
      refcount++;
    }
    void Release() {
      refcount--;
      if (refcount == 0) {
        delete this;
      }
    }
};

这种接缝非常简单,无需担心。但这不是线程安全的! 这是因为“refcount ++”和“refcount--”不是原子操作,而是两个操作:

  • 从内存中读取引用计数以注册
  • 递增/递减寄存器
  • 将注册中的引用计数写入内存

每个操作都可以被中断,另一个线程可以同时操作相同的引用计数。因此,如果例如两个线程想要递增引用refcount,则会发生以下COULD:

  • 线程A:从内存中读取refcount以注册(refcount:8)
  • 线程A:增量寄存器
    • 上下文更改 -
  • 线程B:从内存中读取refcount以注册(refcount:8)
  • 线程B:增量寄存器
  • 线程B:将寄存器中的refcount写入内存(refcount:9)
    • 上下文更改 -
  • 线程A:从寄存器写入refcount到内存(refcount:9)

结果是:refcount = 9但它应该是10!

这只能通过使用原子操作(即Windows上的InterlockedIncrement()& InterlockedDecrement())来解决。

这个bug简直无法测试!原因是,同时尝试修改同一实例的引用计数并且在该代码之间存在上下文切换的可能性非常小。

但它可能发生! (如果您有多处理器或多核系统,则概率会增加,因为不需要进行上下文切换)。 它会在几天,几周或几个月内发生!

答案 3 :(得分:2)

看起来您正在使用Microsoft工具。微软研究院的一个小组一直致力于专门设计用于消除并发性bug的工具。查看CHESS。其他早期阶段的研究项目是CuzzFeatherlite

VS2010包含一个非常好看的并发分析器,视频为available here.

答案 4 :(得分:1)

正如Len Holgate所提到的,我建议重构(如果需要)并为代码的各个部分创建接口,其中不同的线程与携带状态的对象进行交互。然后可以将代码的这些部分与包含实际功能的代码分开测试。为了验证这样的单元测试,我会考虑使用代码覆盖工具(我使用gcov和lcov)来验证线程安全接口中的所有内容都已被覆盖。

我认为这是验证测试中是否涵盖新代码的一种非常方便的方法。 接下来的步骤是遵循关于如何运行测试的其他答案的建议。

答案 5 :(得分:1)

首先,非常感谢您的回复。对于在不同的论坛上发布的回复,请参阅;

http://www.sqaforums.com/showflat.php?Cat=0&Number=617621&an=0&page=0#Post617621

Testing approach for multi-threaded software

http://www.softwaretestingclub.com/forum/topics/testing-approach-for?xg_source=activity

以及以下邮件列表; software-testing@yahoogroups.com

测试花了比预期更长的时间,因此这个迟到的回复使我得出这样的结论:即使编码非常简单,在现有应用程序中添加多线程在测试方面也可能非常昂贵。对于SQA社区来说,这可能会很有趣,因为越来越多的多线程开发正在进行中。

根据Joe Strazzere的建议,我发现击中错误的最有效方法是通过自动化和不同的输入。我最终在三台PC上进行了这项工作,这些PC在大约六周的时间内连续进行了大量测试。最初,我看到每台PC每天崩溃一到两次。当我跟踪这些时,它最终在三台PC之间每周有一两个,而且我们在过去两周没有任何进一步的问题。在过去的两周里,我们还有一个用户测试版本,并且正在内部使用该软件。

除了在自动化下改变输入外,我还从以下方面得到了很好的结果;

  • 添加一个测试选项,允许从配置文件中读取互斥锁超时,而配置文件又可以由我的自动化控制。

  • 将互斥超时延长到执行一段线程代码所需的典型时间,并在超时时触发调试异常。

  • 与调试器(VS2008)一起运行自动化,以便在出现问题时更有可能将其跟踪。

  • 在没有调试器的情况下运行,以确保调试器没有隐藏其他与时序相关的错误。

  • 针对正常发布,调试和完全优化的构建运行自动化。 FWIW,优化的构建引发了其他构建中不可重现的错误。

发现的虫子类型往往是严重的,例如解除引用无效指针,甚至在调试器下进行了相当多的跟踪。正如其他地方所讨论的那样,SuspendThread和ResumeThread函数最终成为主要元凶,所有这些函数的使用都被互斥体所取代。同样,由于没有超时,所有关键部分都被删除了。关闭文档和退出程序也是一个错误源,在一个实例中,文档被销毁,工作线程仍处于活动状态。为了解决这个问题,每个线程添加一个互斥锁来控制线程的生命周期,并由文档析构函数获取以确保线程按预期终止。

再一次,非常感谢所有详细而多样的回复。下次我参加这类活动时,我会做好准备。