【问题标题】:Unit testing threads: how to confirm that a thread was blocked单元测试线程:如何确认线程被阻塞
【发布时间】:2021-03-24 23:44:34
【问题描述】:

我正在为多线程系统编写一组单元测试。我想编写一个测试,确认线程 B 被线程 A 阻塞。我当前的解决方案(我知道这是不正确的)是让线程 A 休眠一段时间,然后让主线程确定线程 B至少花了那么长时间才能完成。我意识到这个测试并不是真的有效,因为线程 B 可能只是被系统调度出来并且需要很长时间才能运行,这与被阻塞无关。


更多信息,基于 cmets:

我不能详细说明,但是 $dayjob 已经实现了自己的任务线程系统,与例如没有太大不同。 Android 的 AsyncTask 和我的任务是(明白吗?)为它编写单元测试。我已经编写了十几个我很满意的单元测试,并且满意是完全确定的。但是只有几个单元测试让我望而却步。在少数情况下,线程需要等待特定条件,我需要确认它实际上是在该条件下被阻塞的,而不是碰巧在错误的时间休眠。

我提出的测试不会产生任何错误的失败,但如果调度程序碰巧在错误的时间让线程进入睡眠状态,仍然可能产生错误的成功。


还有更多信息:我不是在测试使用线程库的代码,而是在测试库本身。

【问题讨论】:

  • “阻塞”就像“等待锁”还是什么?
  • 根据经验,从来没有发现这样的测试要求是必要或有用的——如果线程卡住或循环,通常很明显。 TBH,在多线程和/或分布式系统上,我从来没有发现单元测试作为一个整体是必要或有用的:) 我担心任何这样的工具都会引入比它所能解决的更多的问题,但显然我有没有经历过每一个测试环境。我不羡慕你,祝你好运...:)
  • 在某些全局数据或其他数据需要受保护访问的情况下,大多数操作系统都提供带有超时的锁定等待,因此线程很容易检测到疯狂的锁定等待并抱怨它,(例如,带有合适的日志消息)。这样的代码通常可以留在系统测试中,甚至可以交付,构建没有性能损失,因此 QA 不能抱怨交付的构建不是经过测试的构建:)
  • '然后由主线程来确定'....是的,我担心你的设计正在接近一个没有足够引擎动力逃逸的事件视界。像“完成”这样的短语让我担心,我希望你已经设法避免了“创建/终止/加入”线程设计的超大质量黑洞,正如 umm 所鼓励的那样......“低于最佳”pthread 等库:)
  • 一般意义上的“被阻止”。一个测试在同一个线程队列上启动了两个任务,我想确认第二个任务直到第一个完成才运行。另一项测试确认任务在互斥体上等待。另一个线程在条件变量上等待。

标签: c++ multithreading unit-testing synchronous


【解决方案1】:

多线程代码的测试是一个非常有趣的领域,IMO。我会说你专注于太低级的事情 - 你试图检测“线程 B 被线程 A 阻塞”。您可能还有很多其他问题(竞争条件、数据竞争),例如没有用于存储/加载的适当内存屏障的共享变量。这就是为什么我更愿意在以下两个方面专注于您自己的程序的功能/状态:

  1. 测试期间未达到意外状态(不存在包括竞争条件或数据竞争在内的相关错误)
  2. 测试按预期取得进展(不会因任何原因导致意外的长时间等待/阻塞/死锁,包括竞争条件或数据竞争)

例如,在单生产者/单消费者队列测试的情况下,我们编写一个有 2 个线程的测试。一个线程放置 N 个(最好是数百万个)值(例如,从 1 到 N 的数字)。另一个线程从队列中取出 N 个值。

为了确保我们没有竞争条件或数据竞争,消费者可能会检查每个值是否等于前一个值 + 1。这就是我们检查预期状态的方式。

为了确保我们取得进展,生产者可能会在每次 put() 之后增加一个 PutCounter。消费者 - 每个 take() 之后的 TakeCounter。测试完成后,我们检查我们最终的进度不变量PutCounter == TakeCounter == N。由于我们可能有一个影响进度的bug,我们应该在测试中设置一个合理的Timeout。

由于此类测试需要大量时间才能运行,因此我已在开发人员端禁用它们,并且仅在启用 CI 时禁用它们。

对于您的特定情况,我们可能会测试测试在超时期间没有取得进展,并且仅用于调查/修复错误,我们进行线程转储并看到发生这种情况是因为“线程 B 被线程 A 阻塞”。想想每个线程应该做什么?对于每个线程或整个系统来说,“进度”意味着什么?然后准备一个测试场景并通过超时验证是否达到了最终进度。

【讨论】:

    【解决方案2】:

    ...另一个测试确认任务在互斥体上等待。

    不要浪费时间测试库。专注于测试您自己的代码。如果您需要证明您的代码在某些条件下调用了某个对象(例如,给定的std::mutex)的某个方法(例如lock),那么做到这一点的方法不是通过验证调用线程居然被屏蔽了。测试它的方法是通过 injectingtest double(又名“模拟对象”)来测试互斥体。

    您创建一个符合mutex API 的对象,即测试替身或“模拟”,但它实际上不是互斥锁。然后,您的测试函数设置被测单元锁定互斥锁的具体条件,它调用您的单元,然后查询模拟互斥锁对象以询问“您的lock() 方法是否被调用?”

    设置特定条件可能需要传入其他“测试替身”对象。同样,根据您正在测试的单元的复杂性,您可能必须传入其他测试双重对象,以防止它在调用 lock() 函数后使测试工具崩溃。

    大多数面向 OO 语言的单元测试框架都包含一些用于轻松创建测试替身的方法。

    【讨论】:

      猜你喜欢
      • 2014-10-08
      • 1970-01-01
      • 2014-01-20
      • 2012-01-25
      • 1970-01-01
      • 2010-10-07
      • 2021-03-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多