Fork me on GitHub

前几节已经介绍了Tpool里面基本所有的实现细节,接下来我会谈谈我在实现Tpool的过程中的一些测试经验。

首先,多线程程序有一个最难测试的地方,就是他的不可预测性。也就是说在程序运行的过程中,没有办法准确的知道当前线程是哪一个,以及运行到哪里。因为这些都是跟系统当前的调度策略和环境有关的。这也成为了多线程程序最难测试和调试的一点,当然现在的debugger都有一些命令可以支持线程调度的限制,但是这仍然没有降低编程的难度。

所以除了手工的测试之外,最重要的保障就是编写单元测试。但是单元测试仍然具有不可预测性,而作为程序员,我们应该要在单元测试的时候要尽量去重现线程的竞态条件。而我在这里使用sleep来实现。

有一个点我需要说明一下,我觉得多线程程序里面如果只是针对运算的结果来检查函数是否做了预期的工作的话,是远远达不到测试的要求的。因为结果正确不一定表示函数没有出问题。比如说一个线程池函数的任务是往线程池里面加入任务并执行,如果我测试的时候加入两个对全局变量i执行%2B%2Bi的任务,再加入两个对这个变量执行–i的任务,那么在运行结束之后检查i等于初始值是不能说明线程池正常执行的。因为有可能在执行++i的时候,任务没有加锁,而导致两个任务同时读到了同一个值,所以执行两个任务之后i的值只比之前多了1,而不是2。但是之后两个–i也发生了同样的情况,也只是对i剪了1,所以最终结果还是和预期的一样。所以在测试多线程的时候,要把测试的范围定位到最细的粒度上,而且要尽量的去创造出错的环境。

比如我在测试WorkerThread的时候,要测试Cancel这个方法。而Cancel这个方法的定义是Cancel从开始调用一直block住,直到WorkerThread结束执行之后。

这个Cancel会在什么情况下会出现问题?或者说我的Cancel函数是在什么情况下才有用呢?最重要的场景是当线程正在运行,而这个时候我从另外一个线程调用Cancel。而在Cancel之后的任务将不会执行。

那么怎么重现这种条件呢?

首先明确要测试的条件是下面两个:

  1. 从调用Cancel开始到结束,线程从开始运行的状态到结束的状态。
  2. Cancel之后,线程不再执行任务。

上面的条件,可以用下面的场景来测试:一个任务的运行时间是2秒,且在两秒的最后会对一个全局变量i进行+1操作。往任务队列里面加入两个这样的任务。而我从主线程里面睡眠1秒之后,对工作者线程执行Cancel调用。

对于条件1,如果Cancel没有等待到线程结束就返回,那么在返回之后,全局变量i的值应该和线程运行之前没有变化。如果是正确执行的话,那么值应该是改变了的,并且比之前应该是大1。也就是只执行了一个任务,而第二个任务没有执行就返回了。

而对于条件2,就是在线程结束后,i的值仍然是和Cancel之后一样,并且任务队列里面应该还有一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TEST(WorkerThread, test_Cancel)
{
  int counter = 0;
  TaskQueueBase::Ptr q(new LinearTaskQueue);
  {
	WorkerThread t(q);
	q->Push(TaskBase::Ptr(new TestTask(counter)));
	q->Push(TaskBase::Ptr(new TestTask(counter)));
	sleep(1);
	t.Cancel();
	// expect WorkerThread run only one task
	ASSERT_EQ(1, counter);
  }
  ASSERT_EQ(1, counter);
  ASSERT_EQ(1, q->Size());
}

可以看到在上面的单元测试里面,我通过故意的构造时序上的冲突来测试我的函数有没有达到我的预期,从而达到测试的目的。

至于活跃性测试[1],我暂时还没有在我的单元测试里面明确的去测试这一点。只是在测试过程中有不可预期hang住情况下回去看对应的单元测试。

References

  1. 《JAVA并发编程实战》

知识共享许可协议
作品airekans创作,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。


blog comments powered by Disqus

Published

23 May 2012

Tags