【问题标题】:How should I unit test multithreaded code?我应该如何对多线程代码进行单元测试?
【发布时间】:2010-09-05 22:40:19
【问题描述】:

到目前为止,我已经避免了测试多线程代码的噩梦,因为它看起来像是一个雷区。我想问一下人们是如何测试依赖线程成功执行的代码的,或者人们是如何测试那些仅在两个线程以给定方式交互时才出现的问题的?

对于当今的程序员来说,这似乎是一个非常关键的问题,将我们的知识集中在这个恕我直言上会很有用。

【问题讨论】:

  • 我正在考虑就这个完全相同的问题发布一个问题。虽然威尔在下面提出了许多好的观点,但我认为我们可以做得更好。我同意没有单一的“方法”可以干净地处理这个问题。然而,“尽你所能进行测试”将标准设置得非常低。我会带着我的发现回来。
  • 在 Java 中:包 java.util.concurrent 包含一些众所周知的类,它们可能有助于编写确定性 JUnit-Tests。看看-CountDownLatch-Semaphore-Exchanger
  • 能否提供您之前的单元测试相关问题的链接?
  • 我认为重要的是要注意这个问题已经有 8 年历史了,同时应用程序库已经走过了很长的路。在“现代时代”(2016 年)中,多线程开发主要出现在嵌入式系统中。但是,如果您正在使用桌面或手机应用程序,请先探索替代方案。 .NET 等应用程序环境现在包括用于管理或大大简化大约 90% 的常见多线程场景的工具。 (异步/等待、PLinq、IObservable、TPL...)。多线程代码很难。如果你不重新发明轮子,你就不必重新测试它。

标签: multithreading unit-testing


【解决方案1】:

看,没有简单的方法可以做到这一点。我正在开发一个本质上是多线程的项目。事件来自操作系统,我必须同时处理它们。

处理测试复杂的多线程应用程序代码的最简单方法是:如果测试太复杂,则说明您做错了。如果您有一个有多个线程作用于它的实例,并且您无法测试这些线程相互交叉的情况,那么您的设计需要重做。既简单又复杂。

有很多方法可以为多线程编程,以避免线程同时通过实例运行。最简单的方法是使所有对象不可变。当然,这通常是不可能的。因此,您必须在设计中识别线程与同一实例交互的那些位置,并减少这些位置的数量。通过这样做,您可以隔离一些实际发生多线程的类,从而降低测试系统的整体复杂性。

但是您必须意识到,即使这样做,您仍然无法测试两个线程相互踩踏的所有情况。为此,您必须在同一个测试中同时运行两个线程,然后准确控制它们在任何给定时刻正在执行的行。你能做的最好的就是模拟这种情况。但这可能需要您专门为测试编写代码,而这充其量只是迈向真正解决方案的一半。

可能测试代码是否存在线程问题的最佳方法是对代码进行静态分析。如果您的线程代码不遵循有限的线程安全模式集,那么您可能会遇到问题。我相信 VS 中的代码分析确实包含一些线程知识,但可能不多。

看,就目前的情况(并且可能会在未来的好时机),测试多线程应用程序的最佳方法是尽可能降低线程代码的复杂性。尽量减少线程交互的区域,尽可能进行测试,并使用代码分析来识别危险区域。

【讨论】:

  • 如果您处理允许它的语言/框架,代码分析会很棒。 EG:Findbugs 会发现非常简单易用的静态变量共享并发问题。它找不到的是单例设计模式,它假定所有对象都可以多次创建。对于像 Spring 这样的框架,这个插件严重不足。
  • 实际上有一种治疗方法:活动对象。 drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
  • 虽然这是个好建议,但我仍然想问:“我如何测试那些需要多线程的最小区域?”
  • “如果它太复杂而无法测试,那么你做错了” - 我们都必须深入研究我们没有编写的遗留代码。这一观察结果对任何人都有什么帮助?
  • 静态分析可能是个好主意,但它不是测试。这篇文章真的没有回答这个问题,关于如何测试。
【解决方案2】:

这个问题发布已经有一段时间了,但仍然没有答案......

kleolb02 的回答很好。我会尝试更详细的。

有一种方法,我为 C# 代码练习。对于单元测试,您应该能够编写可重现测试,这是多线程代码中的最大挑战。因此,我的回答旨在将异步代码强制放入测试工具中,该工具同步工作。

这是 Gerard Meszardos 的书“xUnit Test Patterns”中的一个想法,被称为“Humble Object”(第 695 页):您必须将核心逻辑代码和任何闻起来像异步代码的东西分开。这将产生一个用于核心逻辑的类,该类同步工作。

这使您能够以同步的方式测试核心逻辑代码。您可以绝对控制您在核心逻辑上执行的调用的时间,因此可以进行 可重现 测试。这就是分离核心逻辑和异步逻辑的好处。

这个核心逻辑需要被另一个类包裹起来,该类负责异步接收对核心逻辑的调用并将这些调用委派到核心逻辑。生产代码只能通过该类访问核心逻辑。因为这个类应该只委托调用,所以它是一个非常“愚蠢”的类,没有太多逻辑。因此,您可以将这个异步工作类的单元测试保持在最低限度。

任何高于(测试类之间的交互)都是组件测试。同样在这种情况下,如果您坚持“Humble Object”模式,您应该能够绝对控制时间。

【讨论】:

  • 但有时如果线程之间相互配合良好,也应该进行一些测试,对吧?在阅读您的答案后,我绝对会将核心逻辑与异步部分分开。但我仍然会通过异步接口和 work-on-all-threads-have-been-done 回调来测试逻辑。
  • 这对于具有某种形式的并发但实际上并不相互影响的单线程程序和算法来说似乎很棒。我认为它不能很好地测试真正的并行算法。
【解决方案3】:

确实很难!在我的 (C++) 单元测试中,我按照所使用的并发模式将其分为几类:

  1. 对在单线程中运行且不支持线程的类进行单元测试——简单,像往常一样测试。

  2. Monitor objects(在调用者的控制线程中执行同步方法)的单元测试公开了一个同步的公共 API——实例化多个执行 API 的模拟线程。构建锻炼被动对象内部条件的场景。包括一个运行时间较长的测试,它基本上可以在很长一段时间内从多个线程中击败它。我知道这是不科学的,但它确实建立了信心。

  3. Active objects(封装了自己的线程或控制线程的那些)的单元测试——类似于上面的#2,但根据类设计而有所不同。公共 API 可能是阻塞的或非阻塞的,调用者可能会获得期货,数据可能会到达队列或需要出队。这里有许多可能的组合;白盒子走了。仍然需要多个模拟线程来调用被测对象。

顺便说一句:

在我进行的内部开发人员培训中,我教授 Pillars of Concurrency 和这两种模式作为思考和分解并发问题的主要框架。显然有更高级的概念,但我发现这套基础知识有助于让工程师远离困境。如上所述,它还导致代码更易于单元测试。

【讨论】:

    【解决方案4】:

    近年来,在为多个项目编写线程处理代码时,我曾多次遇到过这个问题。我提供了一个较晚的答案,因为大多数其他答案虽然提供了替代方案,但实际上并没有回答有关测试的问题。我的回答是针对没有多线程代码替代方案的情况。为了完整起见,我确实涵盖了代码设计问题,但也讨论了单元测试。

    编写可测试的多线程代码

    首先要做的是将您的生产线程处理代码与所有进行实际数据处理的代码分开。这样,数据处理可以作为单线程代码进行测试,而多线程代码唯一要做的就是协调线程。

    要记住的第二件事是多线程代码中的错误是概率性的;最不常出现的错误是会潜入生产的错误,即使在生产中也难以重现,因此会导致最大的问题。出于这个原因,快速编写代码然后调试它直到它工作的标准编码方法对于多线程代码来说是一个坏主意。这将导致代码中容易的错误被修复而危险的错误仍然存​​在。

    相反,在编写多线程代码时,您必须以一开始就避免编写错误的态度编写代码。如果你已经正确地删除了数据处理代码,线程处理代码应该足够小——最好是几行,最坏是几十行——你有机会在不写错误的情况下编写它,当然也不会写很多错误,如果您了解线程,请慢慢来,并且要小心。

    为多线程代码编写单元测试

    一旦尽可能仔细地编写多线程代码,仍然值得为该代码编写测试。测试的主要目的不是测试高度依赖于时间的竞争条件错误——不可能重复地测试这种竞争条件——而是测试你防止此类错误的锁定策略是否允许多个线程按预期交互.

    要正确测试正确的锁定行为,测试必须启动多个线程。为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生。我们不想在测试中对线程进行外部同步,因为这将掩盖生产中可能发生的线程未外部同步的错误。剩下的就是使用时间延迟来进行线程同步,这是我在必须编写多线程代码测试时成功使用的技术。

    如果延迟太短,则测试会变得脆弱,因为微小的时间差异(例如可能运行测试的不同机器之间)可能会导致时间关闭和测试失败。我通常所做的是从导致测试失败的延迟开始,增加延迟以使测试在我的开发机器上可靠地通过,然后将延迟加倍,这样测试就有很大的机会在其他机器上通过。这确实意味着测试将花费大量时间,尽管根据我的经验,仔细的测试设计可以将时间限制在不超过十几秒。由于您的应用程序中不应该有很多地方需要线程协调代码,因此您的测试套件应该可以接受。

    最后,跟踪您的测试发现的错误数量。如果您的测试有 80% 的代码覆盖率,那么它有望捕获大约 80% 的错误。如果您的测试设计得很好但没有发现任何错误,那么您很有可能没有其他只会出现在生产环境中的错误。如果测试发现一两个错误,您可能仍然很幸运。除此之外,您可能需要考虑仔细审查甚至完全重写您的线程处理代码,因为代码可能仍然包含隐藏的错误,这些错误在代码投入生产之前很难找到,而且非常那时很难修复。

    【讨论】:

    • 测试只能揭示错误的存在,而不是它们的缺失。最初的问题是关于 2 线程问题,在这种情况下,可能会进行详尽的测试,但通常情况并非如此。对于最简单的场景之外的任何事情,您可能不得不咬紧牙关并使用正式的方法 - 但不要跳过单元测试!编写正确的多线程代码首先是困难的,但同样困难的问题是在未来防止回归。
    • 对最不了解的方式之一的惊人总结。你的回答是对人们普遍忽视的真正隔离的打击。
    • 十几秒是相当长的时间,即使你只有几百个这样长度的测试......
    • @TobySpeight 与普通单元测试相比,这些测试要长一些。我发现,如果线程代码设计得尽可能简单,那么六个测试就绰绰有余了——需要几百个多线程测试几乎肯定会表明线程安排过于复杂。
    • 这是一个很好的论据,可以让你的线程逻辑尽可能与功能分离(我知道,说起来容易做起来难)。并且,如果可能的话,将测试套件分解为“每次更改”和“预提交”集(这样您的每分钟测试不会受到太大影响)。
    【解决方案5】:

    我在测试多线程代码时也遇到了严重的问题。然后我在 Gerard Meszaros 的“xUnit 测试模式”中找到了一个非常酷的解决方案。他描述的模式被称为Humble object

    基本上,它描述了如何将逻辑提取到一个独立的、易于测试的组件中,该组件与其环境分离。测试完这个逻辑,就可以测试复杂的行为了(多线程、异步执行等)

    【讨论】:

      【解决方案6】:

      周围有一些非常好的工具。下面是一些 Java 的总结。

      一些好的静态分析工具包括FindBugs(提供一些有用的提示)、JLintJava Pathfinder(JPF & JPF2)和Bogor

      MultithreadedTC 是一个很好的动态分析工具(集成到 JUnit 中),您必须在其中设置自己的测试用例。

      来自 IBM Research 的ConTest 很有趣。它通过插入各种线程修改行为(例如睡眠和屈服)来检测您的代码,以尝试随机发现错误。

      SPIN 是一个非常酷的工具,用于对 Java(和其他)组件进行建模,但您需要有一些有用的框架。它很难按原样使用,但如果你知道如何使用它,它就会非常强大。不少工具在后台使用 SPIN。

      MultithreadedTC 可能是最主流的,但上面列出的一些静态分析工具绝对值得一看。

      【讨论】:

        【解决方案7】:

        Awaitility 也可以帮助您编写确定性单元测试。它允许您等到系统中某处的某个状态被更新。例如:

        await().untilCall( to(myService).myMethod(), greaterThan(3) );
        

        await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));
        

        它还支持 Scala 和 Groovy。

        await until { something() > 4 } // Scala example
        

        【讨论】:

        • 等待非常棒——正是我想要的!
        【解决方案8】:

        另一种(有点)测试线程代码和非常复杂的系统的方法是通过Fuzz Testing。 它不是很好,它不会找到所有东西,但它可能很有用并且操作简单。

        引用:

        模糊测试或模糊测试是一种软件测试技术,可为程序的输入提供随机数据(“模糊”)。如果程序失败(例如,由于崩溃,或由于内置代码断言失败),则可以记录缺陷。模糊测试的最大优点是测试设计非常简单,并且没有对系统行为的先入之见。

        ...

        模糊测试通常用于采用黑盒测试的大型软件开发项目。这些项目通常有开发测试工具的预算,而模糊测试是提供高收益成本比的技术之一。

        ...

        但是,模糊测试不能替代穷举测试或形式化方法:它只能提供系统行为的随机样本,并且在许多情况下,通过模糊测试可能只能证明一个软件可以处理异常而不会崩溃,而不是行为正确。因此,模糊测试只能被视为一种错误发现工具,而不是质量保证。

        【讨论】:

          【解决方案9】:

          如前所述,测试 MT 代码的正确性是一个相当困难的问题。最后归结为确保代码中没有错误同步的数据竞争。这样做的问题是线程执行(交错)有无数种可能性,您对此没有太多控制权(不过,请务必阅读this 文章)。在简单的场景中,可能可以通过推理来实际证明正确性,但通常情况并非如此。特别是如果您想避免/最小化同步,而不是选择最明显/最简单的同步选项。

          我遵循的一种方法是编写高度并发的测试代码,以便可能发生潜在的未被检测到的数据竞争。然后我运行这些测试一段时间 :) 我曾经偶然发现一些计算机科学家在演讲中展示了一种可以执行此操作的工具(从规范中随机设计测试,然后同时疯狂地运行它们,检查定义的不变量被打破)。

          顺便说一句,我认为这里没有提到测试 MT 代码的这一方面:识别可以随机检查的代码的不变量。不幸的是,找到这些不变量也是一个相当困难的问题。此外,它们可能不会在执行期间一直保持,因此您必须找到/执行您可以期望它们为真的执行点。将代码执行到这样的状态也是一个难题(并且本身可能会引发并发问题。哇,这太难了!

          一些有趣的阅读链接:

          【讨论】:

          • 作者是指测试中的随机化。可能是QuickCheck,它已被移植到多种语言。您可以观看有关并发系统的此类测试的讨论here
          【解决方案10】:

          我已经做了很多这样的事情,是的,这很糟糕。

          一些提示:

          • GroboUtils 用于运行多个测试线程
          • alphaWorks ConTest 用于检测类以导致交叉在迭代之间发生变化
          • 创建一个throwable 字段并在tearDown 中检查它(参见清单1)。如果您在另一个线程中捕获到错误异常,只需将其分配给 throwable。
          • 我在清单 2 中创建了 utils 类,发现它非常宝贵,尤其是 waitForVerify 和 waitForCondition,这将大大提高您的测试性能。
          • 在您的测试中充分利用AtomicBoolean。它是线程安全的,您通常需要一个最终引用类型来存储来自回调类等的值。请参见清单 3 中的示例。
          • 确保始终让您的测试超时(例如,@Test(timeout=60*1000)),因为并发测试有时会在中断时永远挂起。

          清单 1:

          @After
          public void tearDown() {
              if ( throwable != null )
                  throw throwable;
          }
          

          清单 2:

          import static org.junit.Assert.fail;
          import java.io.File;
          import java.lang.reflect.InvocationHandler;
          import java.lang.reflect.Proxy;
          import java.util.Random;
          import org.apache.commons.collections.Closure;
          import org.apache.commons.collections.Predicate;
          import org.apache.commons.lang.time.StopWatch;
          import org.easymock.EasyMock;
          import org.easymock.classextension.internal.ClassExtensionHelper;
          import static org.easymock.classextension.EasyMock.*;
          
          import ca.digitalrapids.io.DRFileUtils;
          
          /**
           * Various utilities for testing
           */
          public abstract class DRTestUtils
          {
              static private Random random = new Random();
          
          /** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
           * default max wait and check period values.
           */
          static public void waitForCondition(Predicate predicate, String errorMessage) 
              throws Throwable
          {
              waitForCondition(null, null, predicate, errorMessage);
          }
          
          /** Blocks until a condition is true, throwing an {@link AssertionError} if
           * it does not become true during a given max time.
           * @param maxWait_ms max time to wait for true condition. Optional; defaults
           * to 30 * 1000 ms (30 seconds).
           * @param checkPeriod_ms period at which to try the condition. Optional; defaults
           * to 100 ms.
           * @param predicate the condition
           * @param errorMessage message use in the {@link AssertionError}
           * @throws Throwable on {@link AssertionError} or any other exception/error
           */
          static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
              Predicate predicate, String errorMessage) throws Throwable 
          {
              waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
                  public void execute(Object errorMessage)
                  {
                      fail((String)errorMessage);
                  }
              }, errorMessage);
          }
          
          /** Blocks until a condition is true, running a closure if
           * it does not become true during a given max time.
           * @param maxWait_ms max time to wait for true condition. Optional; defaults
           * to 30 * 1000 ms (30 seconds).
           * @param checkPeriod_ms period at which to try the condition. Optional; defaults
           * to 100 ms.
           * @param predicate the condition
           * @param closure closure to run
           * @param argument argument for closure
           * @throws Throwable on {@link AssertionError} or any other exception/error
           */
          static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
              Predicate predicate, Closure closure, Object argument) throws Throwable 
          {
              if ( maxWait_ms == null )
                  maxWait_ms = 30 * 1000;
              if ( checkPeriod_ms == null )
                  checkPeriod_ms = 100;
              StopWatch stopWatch = new StopWatch();
              stopWatch.start();
              while ( !predicate.evaluate(null) ) {
                  Thread.sleep(checkPeriod_ms);
                  if ( stopWatch.getTime() > maxWait_ms ) {
                      closure.execute(argument);
                  }
              }
          }
          
          /** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
           * for {@code maxWait_ms}
           */
          static public void waitForVerify(Object easyMockProxy)
              throws Throwable
          {
              waitForVerify(null, easyMockProxy);
          }
          
          /** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
           * max wait time has elapsed.
           * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
           * @param easyMockProxy Proxy to call verify on
           * @throws Throwable
           */
          static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
              throws Throwable
          {
              if ( maxWait_ms == null )
                  maxWait_ms = 30 * 1000;
              StopWatch stopWatch = new StopWatch();
              stopWatch.start();
              for(;;) {
                  try
                  {
                      verify(easyMockProxy);
                      break;
                  }
                  catch (AssertionError e)
                  {
                      if ( stopWatch.getTime() > maxWait_ms )
                          throw e;
                      Thread.sleep(100);
                  }
              }
          }
          
          /** Returns a path to a directory in the temp dir with the name of the given
           * class. This is useful for temporary test files.
           * @param aClass test class for which to create dir
           * @return the path
           */
          static public String getTestDirPathForTestClass(Object object) 
          {
          
              String filename = object instanceof Class ? 
                  ((Class)object).getName() :
                  object.getClass().getName();
              return DRFileUtils.getTempDir() + File.separator + 
                  filename;
          }
          
          static public byte[] createRandomByteArray(int bytesLength)
          {
              byte[] sourceBytes = new byte[bytesLength];
              random.nextBytes(sourceBytes);
              return sourceBytes;
          }
          
          /** Returns <code>true</code> if the given object is an EasyMock mock object 
           */
          static public boolean isEasyMockMock(Object object) {
              try {
                  InvocationHandler invocationHandler = Proxy
                          .getInvocationHandler(object);
                  return invocationHandler.getClass().getName().contains("easymock");
              } catch (IllegalArgumentException e) {
                  return false;
              }
          }
          }
          

          清单 3:

          @Test
          public void testSomething() {
              final AtomicBoolean called = new AtomicBoolean(false);
              subject.setCallback(new SomeCallback() {
                  public void callback(Object arg) {
                      // check arg here
                      called.set(true);
                  }
              });
              subject.run();
              assertTrue(called.get());
          }
          

          【讨论】:

          • 超时是个好主意,但如果测试超时,则该运行中的任何后续结果都是可疑的。超时的测试可能仍然有一些线程在运行,可能会搞砸你。
          【解决方案11】:

          我处理线程组件的单元测试的方式与处理任何单元测试的方式相同,即控制反转和隔离框架。我在 .Net 领域进行开发,开箱即用,线程(除其他外)很难(我会说几乎不可能)完全隔离。

          因此,我编写了看起来像这样(简化)的包装器:

          public interface IThread
          {
              void Start();
              ...
          }
          
          public class ThreadWrapper : IThread
          {
              private readonly Thread _thread;
               
              public ThreadWrapper(ThreadStart threadStart)
              {
                  _thread = new Thread(threadStart);
              }
          
              public Start()
              {
                  _thread.Start();
              }
          }
              
          public interface IThreadingManager
          {
              IThread CreateThread(ThreadStart threadStart);
          }
          
          public class ThreadingManager : IThreadingManager
          {
              public IThread CreateThread(ThreadStart threadStart)
              {
                   return new ThreadWrapper(threadStart)
              }
          }
          

          从那里,我可以轻松地将 IThreadingManager 注入到我的组件中,并使用我选择的隔离框架使线程在测试期间按预期运行。

          到目前为止,这对我来说效果很好,我对线程池、System.Environment、Sleep 等中的事物使用相同的方法。

          【讨论】:

          • +1。很遗憾 dotnet still 对这种方法的支持如此差。必须为诸如 Task.Delay 之类的平凡事物编写包装器
          【解决方案12】:

          Pete Goodliffeunit testing of threaded 代码上有一个系列。

          这很难。我采取了更简单的方法,并尝试将线程代码从实际测试中抽象出来。皮特确实提到我这样做的方式是错误的,但我要么正确地分开,要么我很幸运。

          【讨论】:

          • 我阅读了目前发表的两篇文章,并没有发现它们很有帮助。他只是谈论困难,没有给出太多具体的建议。也许以后的文章会改进。
          【解决方案13】:

          对于 Java,请查看 JCIP 的第 12 章。有一些编写确定性多线程单元测试的具体示例,至少可以测试并发代码的正确性和不变性。

          用单元测试“证明”线程安全性要危险得多。我的信念是,通过在各种平台/配置上进行自动化集成测试可以更好地实现这一点。

          【讨论】:

            【解决方案14】:

            看看我的相关答案

            Designing a Test class for a custom Barrier

            它偏向于 Java,但对选项进行了合理的总结。

            总之,虽然(IMO)它不是使用一些花哨的框架来确保正确性,而是你如何设计你的多线程代码。拆分关注点(并发性和功能性)对提高信心大有帮助。 Growing Object Orientated Software Guided By Tests 比我能更好地解释一些选项。

            静态分析和形式化方法(参见Concurrency: State Models and Java Programs)是一种选择,但我发现它们在商业开发中的用途有限。

            不要忘记,任何加载/浸泡式测试很少保证能突出问题。

            祝你好运!

            【讨论】:

            • 您还应该在此处提及您的 tempus-fugit 库,其中 helps write and test concurrent code ;)
            【解决方案15】:

            我喜欢编写两个或多个测试方法以在并行线程上执行,每个测试方法都调用被测对象。我一直在使用 Sleep() 调用来协调来自不同线程的调用顺序,但这并不可靠。它也慢很多,因为你必须睡足够长的时间才能正常计时。

            我从编写 FindBugs 的同一组中找到了 Multithreaded TC Java library。它允许您在不使用 Sleep() 的情况下指定事件的顺序,并且它是可靠的。我还没试过。

            这种方法的最大限制是它只能让您测试您怀疑会导致问题的场景。正如其他人所说,您确实需要将多线程代码隔离为少量简单的类,以便有希望彻底测试它们。

            一旦您仔细测试了您预期会导致麻烦的场景,那么在一段时间内向班级抛出一堆同时请求的不科学测试是寻找意外问题的好方法。

            更新:我使用了多线程 TC Java 库,它运行良好。我还将它的一些功能移植到了一个我称之为TickingTest 的.NET 版本。

            【讨论】:

              【解决方案16】:

              我最近(对于 Java)发现了一个名为 Threadsafe 的工具。它是一个类似于 findbugs 的静态分析工具,但专门用于发现多线程问题。它不能替代测试,但我可以推荐它作为编写可靠的多线程 Java 的一部分。

              它甚至可以捕捉到一些非常微妙的潜在问题,例如类包含、通过并发类访问不安全的对象以及在使用双重检查锁定范例时发现缺失的 volatile 修饰符。

              如果你写多线程Java give it a shot.

              【讨论】:

                【解决方案17】:

                以下文章建议了 2 个解决方案。包装信号量(CountDownLatch)并添加功能,例如从内部线程外部化数据。实现此目的的另一种方法是使用线程池(请参阅兴趣点)。

                Sprinkler - Advanced synchronization object

                【讨论】:

                • 请在此处说明方法,将来外部链接可能会失效。
                【解决方案18】:

                上周我大部分时间都在大学图书馆学习并发代码的调试。核心问题是并发代码是不确定的。通常,学术调试属于以下三个阵营之一:

                1. 事件跟踪/重播。这需要一个事件监视器,然后查看已发送的事件。在 UT 框架中,这将涉及手动发送事件作为测试的一部分,然后进行事后审查。
                2. 可编写脚本。这是您使用一组触发器与正在运行的代码交互的地方。 “在 x > foo,baz()”上。这可以解释为 UT 框架,其中您有一个运行时系统在特定条件下触发给定测试。
                3. 交互式。这显然不适用于自动测试情况。 ;)

                现在,正如上述评论员所注意到的,您可以将并发系统设计为更具确定性的状态。然而,如果你没有正确地做到这一点,你就只能重新设计一个顺序系统。

                我的建议是专注于制定一个非常严格的设计协议,关于什么是线程化的,什么不是线程化的。如果你限制你的界面,使元素之间的依赖最小,那就容易多了。

                祝你好运,继续解决问题。

                【讨论】:

                  【解决方案19】:

                  我有一个不幸的任务是测试线程代码,它们绝对是我写过的最难的测试。

                  在编写测试时,我使用了委托和事件的组合。基本上都是关于使用PropertyNotifyChanged 事件和WaitCallback 或某种ConditionalWaiter 进行轮询。

                  我不确定这是否是最好的方法,但它对我来说很有效。

                  【讨论】:

                    【解决方案20】:

                    假设在“多线程”代码下的意思是

                    • 有状态且可变
                    • AND 被多个线程访问/修改 同时

                    换句话说,我们正在谈论测试自定义有状态线程安全类/方法/单元 - 这在当今应该是非常罕见的野兽。

                    由于这种野兽很少见,首先我们需要确保有所有正当的借口来写它。

                    第 1 步。考虑在同一同步上下文中修改状态。

                    今天很容易编写可组合的并发和异步代码,其中 IO 或其他慢速操作被卸载到后台,但共享状态在一个同步上下文中更新和查询。例如.NET 等中的 async/await 任务和 Rx - 它们都可以通过设计进行测试,“真实”任务和调度程序可以替代以使测试具有确定性(但这超出了问题的范围)。

                    这听起来可能很受限制,但这种方法效果出奇的好。可以用这种风格编写整个应用程序,而无需使任何状态线程安全(我愿意)。

                    第 2 步。 如果在单个同步上下文中操作共享状态是绝对不可能的。

                    确保轮子没有被重新发明/绝对没有可以适应这项工作的标准替代方案。代码应该很可能具有很强的凝聚力并包含在一个单元中,例如很有可能它是一些标准线程安全数据结构(如哈希映射或集合或其他)的特例。

                    注意:如果代码很大/跨越多个类并且需要多线程状态操作,那么很有可能设计不好,重新考虑第 1 步

                    第 3 步。如果达到此步骤,那么我们需要测试我们自己的自定义有状态线程安全类/方法/单元

                    老实说:我从来不需要为这样的代码编写适当的测试。大部分时间我都在第 1 步,有时在第 2 步。上一次我必须编写自定义线程安全代码是多年前的事了,那是在我采用单元测试之前/可能我不必编写它无论如何,以目前的知识。

                    如果我真的必须测试这样的代码(最后,实际答案)那么我会尝试以下几件事

                    1. 非确定性压力测试。例如同时运行 100 个线程并检查最终结果是否一致。 这对于多用户场景的更高级别/集成测试更为典型,但也可以在单元级别使用。

                    2. 公开一些测试“钩子”,测试可以在其中注入一些代码,以帮助确定一个线程必须在另一个线程之前执行操作的情况。 这么丑,我想不出更好的了。

                    3. 延迟驱动的测试使线程运行并以特定顺序执行操作。严格来说,这样的测试也是非确定性的(有可能会导致系统冻结/停止世界 GC 收集,这可能会扭曲原本精心安排的延迟),它也很丑陋,但可以避免挂钩。

                    【讨论】:

                      【解决方案21】:

                      对于 J2E 代码,我使用 SilkPerformer、LoadRunner 和 JMeter 对线程进行并发测试。他们都做同样的事情。基本上,它们为您提供了一个相对简单的界面来管理他们的代理服务器版本,以便分析 TCP/IP 数据流,并模拟多个用户同时向您的应用服务器发出请求。代理服务器可以让您执行分析请求等操作,方法是在处理请求后显示发送到服务器的整个页面和 URL,以及来自服务器的响应。

                      您可以在不安全的 http 模式中找到一些错误,您至少可以在其中分析正在发送的表单数据,并为每个用户系统地更改这些数据。但真正的测试是在 https(安全套接字层)中运行时。然后,您还必须应对系统地更改会话和 cookie 数据,这可能有点复杂。

                      我在测试并发性时发现的最好的错误是,当我发现开发人员在登录时依赖 Java 垃圾收集来关闭登录时建立的到 LDAP 服务器的连接请求。这导致了当试图分析服务器瘫痪时发生的情况时,用户会暴露于其他用户的会话和非常混乱的结果,每隔几秒钟几乎无法完成一项事务。

                      最后,您或其他人可能不得不认真分析代码中的错误,就像我刚才提到的那样。跨部门的公开讨论,就像我们展开上述问题时发生的那样,是最有用的。但这些工具是测试多线程代码的最佳解决方案。 JMeter 是开源的。 SilkPerformer 和 LoadRunner 是专有的。如果您真的想知道您的应用程序是否是线程安全的,那么大男孩就是这样做的。我专业地为非常大的公司做过这件事,所以我不是在猜测。我是根据个人经验说的。

                      请注意:理解这些工具确实需要一些时间。这不是简单地安装软件和启动 GUI 的问题,除非您已经接触过多线程编程。我试图确定需要理解的 3 个关键领域类别(表单、会话和 cookie 数据),希望至少从理解这些主题开始可以帮助您专注于快速结果,而不是必须通读完整的文档。

                      【讨论】:

                        【解决方案22】:

                        并发是内存模型、硬件、缓存和我们的代码之间复杂的相互作用。在 Java 的情况下,至少此类测试主要由jcstress 部分解决。众所周知,该库的创建者是许多 JVM、GC 和 Java 并发特性的作者。

                        但即使是这个库也需要对 Java 内存模型规范有很好的了解,这样我们才能准确地知道我们正在测试什么。但我认为这项工作的重点是 mircobenchmarks。不是大型的商业应用程序。

                        【讨论】:

                          【解决方案23】:

                          有一篇关于该主题的文章,使用Rust作为示例代码中的语言:

                          https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

                          总而言之,诀窍是编写并发逻辑,以便使用通道和 condvar 等工具,使其对涉及多个执行线程的非确定性具有鲁棒性。

                          那么,如果这就是您构建“组件”的方式,那么测试它们的最简单方法是使用通道向它们发送消息,然后阻止其他通道以断言组件发送某些预期的消息。

                          链接到的文章完全使用单元测试编写。

                          【讨论】:

                            【解决方案24】:

                            它并不完美,但我为我在 C# 中的测试编写了这个助手:

                            using System;
                            using System.Collections.Generic;
                            using System.Threading;
                            using System.Threading.Tasks;
                            
                            namespace Proto.Promises.Tests.Threading
                            {
                                public class ThreadHelper
                                {
                                    public static readonly int multiThreadCount = Environment.ProcessorCount * 100;
                                    private static readonly int[] offsets = new int[] { 0, 10, 100, 1000 };
                            
                                    private readonly Stack<Task> _executingTasks = new Stack<Task>(multiThreadCount);
                                    private readonly Barrier _barrier = new Barrier(1);
                                    private int _currentParticipants = 0;
                                    private readonly TimeSpan _timeout;
                            
                                    public ThreadHelper() : this(TimeSpan.FromSeconds(10)) { } // 10 second timeout should be enough for most cases.
                            
                                    public ThreadHelper(TimeSpan timeout)
                                    {
                                        _timeout = timeout;
                                    }
                            
                                    /// <summary>
                                    /// Execute the action multiple times in parallel threads.
                                    /// </summary>
                                    public void ExecuteMultiActionParallel(Action action)
                                    {
                                        for (int i = 0; i < multiThreadCount; ++i)
                                        {
                                            AddParallelAction(action);
                                        }
                                        ExecutePendingParallelActions();
                                    }
                            
                                    /// <summary>
                                    /// Execute the action once in a separate thread.
                                    /// </summary>
                                    public void ExecuteSingleAction(Action action)
                                    {
                                        AddParallelAction(action);
                                        ExecutePendingParallelActions();
                                    }
                            
                                    /// <summary>
                                    /// Add an action to be run in parallel.
                                    /// </summary>
                                    public void AddParallelAction(Action action)
                                    {
                                        var taskSource = new TaskCompletionSource<bool>();
                                        lock (_executingTasks)
                                        {
                                            ++_currentParticipants;
                                            _barrier.AddParticipant();
                                            _executingTasks.Push(taskSource.Task);
                                        }
                                        new Thread(() =>
                                        {
                                            try
                                            {
                                                _barrier.SignalAndWait(); // Try to make actions run in lock-step to increase likelihood of breaking race conditions.
                                                action.Invoke();
                                                taskSource.SetResult(true);
                                            }
                                            catch (Exception e)
                                            {
                                                taskSource.SetException(e);
                                            }
                                        }).Start();
                                    }
                            
                                    /// <summary>
                                    /// Runs the pending actions in parallel, attempting to run them in lock-step.
                                    /// </summary>
                                    public void ExecutePendingParallelActions()
                                    {
                                        Task[] tasks;
                                        lock (_executingTasks)
                                        {
                                            _barrier.SignalAndWait();
                                            _barrier.RemoveParticipants(_currentParticipants);
                                            _currentParticipants = 0;
                                            tasks = _executingTasks.ToArray();
                                            _executingTasks.Clear();
                                        }
                                        try
                                        {
                                            if (!Task.WaitAll(tasks, _timeout))
                                            {
                                                throw new TimeoutException($"Action(s) timed out after {_timeout}, there may be a deadlock.");
                                            }
                                        }
                                        catch (AggregateException e)
                                        {
                                            // Only throw one exception instead of aggregate to try to avoid overloading the test error output.
                                            throw e.Flatten().InnerException;
                                        }
                                    }
                            
                                    /// <summary>
                                    /// Run each action in parallel multiple times with differing offsets for each run.
                                    /// <para/>The number of runs is 4^actions.Length, so be careful if you don't want the test to run too long.
                                    /// </summary>
                                    /// <param name="expandToProcessorCount">If true, copies each action on additional threads up to the processor count. This can help test more without increasing the time it takes to complete.
                                    /// <para/>Example: 2 actions with 6 processors, runs each action 3 times in parallel.</param>
                                    /// <param name="setup">The action to run before each parallel run.</param>
                                    /// <param name="teardown">The action to run after each parallel run.</param>
                                    /// <param name="actions">The actions to run in parallel.</param>
                                    public void ExecuteParallelActionsWithOffsets(bool expandToProcessorCount, Action setup, Action teardown, params Action[] actions)
                                    {
                                        setup += () => { };
                                        teardown += () => { };
                                        int actionCount = actions.Length;
                                        int expandCount = expandToProcessorCount ? Math.Max(Environment.ProcessorCount / actionCount, 1) : 1;
                                        foreach (var combo in GenerateCombinations(offsets, actionCount))
                                        {
                                            setup.Invoke();
                                            for (int k = 0; k < expandCount; ++k)
                                            {
                                                for (int i = 0; i < actionCount; ++i)
                                                {
                                                    int offset = combo[i];
                                                    Action action = actions[i];
                                                    AddParallelAction(() =>
                                                    {
                                                        for (int j = offset; j > 0; --j) { } // Just spin in a loop for the offset.
                                                        action.Invoke();
                                                    });
                                                }
                                            }
                                            ExecutePendingParallelActions();
                                            teardown.Invoke();
                                        }
                                    }
                            
                                    // Input: [1, 2, 3], 3
                                    // Ouput: [
                                    //          [1, 1, 1],
                                    //          [2, 1, 1],
                                    //          [3, 1, 1],
                                    //          [1, 2, 1],
                                    //          [2, 2, 1],
                                    //          [3, 2, 1],
                                    //          [1, 3, 1],
                                    //          [2, 3, 1],
                                    //          [3, 3, 1],
                                    //          [1, 1, 2],
                                    //          [2, 1, 2],
                                    //          [3, 1, 2],
                                    //          [1, 2, 2],
                                    //          [2, 2, 2],
                                    //          [3, 2, 2],
                                    //          [1, 3, 2],
                                    //          [2, 3, 2],
                                    //          [3, 3, 2],
                                    //          [1, 1, 3],
                                    //          [2, 1, 3],
                                    //          [3, 1, 3],
                                    //          [1, 2, 3],
                                    //          [2, 2, 3],
                                    //          [3, 2, 3],
                                    //          [1, 3, 3],
                                    //          [2, 3, 3],
                                    //          [3, 3, 3]
                                    //        ]
                                    private static IEnumerable<int[]> GenerateCombinations(int[] options, int count)
                                    {
                                        int[] indexTracker = new int[count];
                                        int[] combo = new int[count];
                                        for (int i = 0; i < count; ++i)
                                        {
                                            combo[i] = options[0];
                                        }
                                        // Same algorithm as picking a combination lock.
                                        int rollovers = 0;
                                        while (rollovers < count)
                                        {
                                            yield return combo; // No need to duplicate the array since we're just reading it.
                                            for (int i = 0; i < count; ++i)
                                            {
                                                int index = ++indexTracker[i];
                                                if (index == options.Length)
                                                {
                                                    indexTracker[i] = 0;
                                                    combo[i] = options[0];
                                                    if (i == rollovers)
                                                    {
                                                        ++rollovers;
                                                    }
                                                }
                                                else
                                                {
                                                    combo[i] = options[index];
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            

                            示例用法:

                            [Test]
                            public void DeferredMayBeBeResolvedAndPromiseAwaitedConcurrently_void0()
                            {
                                Promise.Deferred deferred = default(Promise.Deferred);
                                Promise promise = default(Promise);
                            
                                int invokedCount = 0;
                            
                                var threadHelper = new ThreadHelper();
                                threadHelper.ExecuteParallelActionsWithOffsets(false,
                                    // Setup
                                    () =>
                                    {
                                        invokedCount = 0;
                                        deferred = Promise.NewDeferred();
                                        promise = deferred.Promise;
                                    },
                                    // Teardown
                                    () => Assert.AreEqual(1, invokedCount),
                                    // Parallel Actions
                                    () => deferred.Resolve(),
                                    () => promise.Then(() => { Interlocked.Increment(ref invokedCount); }).Forget()
                                );
                            }
                            

                            【讨论】:

                              【解决方案25】:

                              如果您正在测试简单的 new Thread(runnable).run() 您可以模拟 Thread 以按顺序运行可运行对象

                              例如,如果被测对象的代码像这样调用一个新线程

                              Class TestedClass {
                                  public void doAsychOp() {
                                     new Thread(new myRunnable()).start();
                                  }
                              }
                              

                              然后模拟新线程并按顺序运行可运行参数会有所帮助

                              @Mock
                              private Thread threadMock;
                              
                              @Test
                              public void myTest() throws Exception {
                                  PowerMockito.mockStatic(Thread.class);
                                  //when new thread is created execute runnable immediately 
                                  PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
                                      @Override
                                      public Thread answer(InvocationOnMock invocation) throws Throwable {
                                          // immediately run the runnable
                                          Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
                                          if(runnable != null) {
                                              runnable.run();
                                          }
                                          return threadMock;//return a mock so Thread.start() will do nothing         
                                      }
                                  }); 
                                  TestedClass testcls = new TestedClass()
                                  testcls.doAsychOp(); //will invoke myRunnable.run in current thread
                                  //.... check expected 
                              }
                              

                              【讨论】:

                                【解决方案26】:

                                (如果可能)不要使用线程,使用参与者/活动对象。易于测试。

                                【讨论】:

                                • @OMTheEternity 也许但它仍然是 imo 的最佳答案。
                                【解决方案27】:

                                您可以使用 EasyMock.makeThreadSafe 使测试实例线程安全

                                【讨论】:

                                • 这根本不是测试多线程代码的可行方法。问题不在于测试代码运行多线程,而是您测试通常运行多线程的代码。而且您无法同步所有内容,因为您实际上不再测试数据竞争。
                                猜你喜欢
                                • 1970-01-01
                                • 2010-09-05
                                • 2014-03-25
                                • 2020-05-30
                                • 1970-01-01
                                相关资源
                                最近更新 更多