同步覆盖率

代码并发覆盖率

Chris Dern 和 Roy Patrick Tan

下载示例代码

我们是在另一个 crossroads,行业中越来越多场晶体管加载处理器需要意识到其完整的潜在的多线程的代码。 从桌面计算机 netbooks sport 双核实质上的处理器不小于时, throngs hungry 晶体管的坐下来空闲--crying devour 多线程应用程序。 若要解决并发应用程序的 oncoming 波形,如 Intel 和 Microsoft 的公司比赛将探查器、 框架、 调试器和库的市场。 为多线程应用程序 proliferate,这样太讨论的死锁、 活锁和数据争用会越来越常见整个软件行业。 软件开发人员需要采用的技术和度量标准可以处理多线程软件的新工具。

代码覆盖率和并发

传统的代码覆盖率的度量值,如语句、 块,和分支,无法解决引入并发测试充分性问题。 例如,让我们语句覆盖范围。 虽然一个不完美跃点数语句覆盖率就是常用,并且有助于在程度找出进行了如何进行全面测试您的代码。 作为一个的刷新器语句覆盖率测量执行多少语句的应用程序的代码。 它所使用的多个单元测试工具,简单跃点数,许多软件开发团队包括高语句覆盖范围代码质量的条形。 遗憾的是,语句覆盖率不赋予您任何与在代码中,并发程度被测试的可见性。

负绀轰緥的图 1 显示了一个简单类型实现的一个线程安全队列用 C# 编写的 (本文,我们使用 C# 和.NET 我们的示例,但同步覆盖的思路是适用于本机代码以及)。

我们的示例有只有两个方法:排入队列 Dequeue 和换行.NET 队列 < T >通过周围的每个排队和出列锁中的操作。 我们可能如何测试此队列实现? 以下代码显示了我们的线程安全的队列的一个简单的单元测试:

void SimpleQueueTest() {
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
q.Enqueue(10);
Assert.AreEqual(10, q.Dequeue());
}

请注意此简单测试提供我们 100%语句覆盖,因为这一测试练习我们队列的实现中的每个语句。

但是,等待,我们有问题--队列应该是线程安全,但到目前为止我们所测试使用只有单个线程 ! 我们没有测试重要行为的原因,为什么我们实现此 ThreadSafeQueue 类型在第一个位置。 语句覆盖率告诉我们我们打算同时访问一个类型上使用一个线程时有 100%的覆盖范围。 清楚地,语句覆盖率就是告诉我们不充分的 并发 方面我们进行了测试的代码的多少。 我们丢失了什么?

图 1 C# 中的线程安全队列实现

public class ThreadSafeQueue<T> {
object m_lock = new object();
Queue<T> m_queue = new Queue<T>();
public void Enqueue(T value) {
lock (m_lock) {
m_queue.Enqueue(value);
}
}
public T Dequeue() {
lock (m_lock) {
return m_queue.Dequeue();
}
}
}

我们是什么丢失是隐藏在 lock 语句分支。 设想一下我们替换锁定自定义锁定基元,如简化实现的一个数值调节钮的锁的方法,如下所示:

void Acquire() {
while (!TryAcquire ()) {
// idle for a bit
Spin();
}
}

输入方法调用 TryAcquire,并且如果它未能获取锁 (如 TryAcquire 返回 false),有点旋转再次尝试。 如果我们组合锁定此自定义的代码与我们排入队列的方法时,将的影响语句范围? 下面的代码演示如何我们可以重新排入队列编写的自定义的锁定:

public void Enqueue(T value) {
while (!TryAcquire ()) {
//idle for a bit
Spin();
}
m_queue.Enqueue(value);
Release();
}

现在,如果我们的测试,我们运行,我们突然看到缺少语句覆盖范围。 任何单个线程的测试将错过在数值调节钮语句,因为 TryAcquire 将只返回 false,如果有另一个线程已持有锁。 该方法将数值调节钮唯一方法是如果某些其他线程进入关键节。 也就是我们可以只介绍数值调节钮语句如果存在争用。 lock 语句中该隐式的分支是我们覆盖率漏洞的源。

同步覆盖率

因为我们 don’t 希望任何人锁定语句替换为自己互斥基元 (也我们提倡这样做),我们需要找到一种公开此隐藏分支之上方法,以便我们可以在我们的测试测量发生争用的量。 研究人员在 IBM 提供的脚本调用同步覆盖率这只是可以执行的代码覆盖率模型。

您可以读取纸张下面,更多信息一节中引用,但核心思路是简单。 第一次,我们一个同步基元--例如.NET 监视器类型 (用于在 C# lock 关键字和 SyncLock 关键字在 Visual Basic 中的)。 然后,获得,或者 (在的情况下,说,Monitor.Enter),阻止它是否记录或其他位置被锁在代码中的每个点未能获取锁 (例如使用 Monitor.TryEnter)。 结果意味着出现的争用。 因此,说我们覆盖范围锁通过,它并不足以执行 lock 语句 ;某些其他线程持有锁时,我们还必须执行 lock 语句。

在这种方法适用于本机和托管的应用程序,本文的其余部分将讨论我们托管的解决方案--.NET 称为同步封面的一个原型同步覆盖率工具。

让我们花一些时间来阐明概念我们覆盖率工具使用。 同步点是一个特定的词法调用站点,的调用同步方法。 采用快速回顾我们的示例线程安全的队列 (图 1),与我们的 C# 编译器帽子上,我们看到在排入队列和 Dequeue 锁语句中隐藏的两个类位置。 在任何单个执行时我们感兴趣两种类型的覆盖范围:语句覆盖率,在该方法不会遇到任何争用 ;和争用覆盖率,阻止或等待另一个线程释放它强制方法的位置。
本文的其余部分我们将重点专门 System.Threading.Monitor,.NET 同步稳定的该工作程序。 但是,它是非常有用注意这种方法同样适用的很好地与其他基元,System.Threading.Interlocked.CompareExchange 类似。

圆形 tripping 使用 IL

此同步覆盖率工具我们目标是将透明地检测现有的.NET 程序集,这样我们可以截获该程序集中的每个 Monitor.Enter 调用,并确定是否的同步点出现争用。 代码覆盖率解决方案现在使用大多数类似我们工具应用重写的 IL (在.NET 中间语言) 以生成检测的目标的程序集的版本的技术。 尽管我们执行花费大部分时间在 C# 中通过直接处理此工具应适用于所有.NET 语言编译成 IL 的理论上的该 IL。

虽然有几个可能的技术启用代码,重写包括新发布的常规编译器结构从 Microsoft 研究 (MSR),为简单起见我们选择回到 trusty IL 编译器和 decompiler 组合 ILASM 和 ILDASM。 直接使用纯文本格式文件中 IL 证明阶段发现、 设计和开发使我们能够浏览一个"付薪的-播放"中的设计选项的该项目的是一个巨大的有用我们投入的时间的方法。 我们事实证明,生存能力和实用性,在我们的解决方案的第一次通过一个完全手动的过程组成批处理文件和记事本。 虽然这可能不是一个产品的质量工具的最佳选择,但我们建议此方法重写应用程序的任何类似原型。

与此总体设计决策完成,看我们工具链的快速教程中的图 2 所示。

我们使用 ILDASM,反编译目标程序集到一个文本文件 IL 的说明。 我们然后加载到内存中后面的基于对象的表面的源代码。 然后,我们应用所需的修改,并发出修改的源的该代码的代码 injections 回磁盘。 然后它是只是简单的重新编译与 ILASM,覆盖程序集,并检查它以捕捉任何有点傻无效转换 PeVerify 我们可能应用了。 我们的覆盖范围程序集后,我们执行正常,我们测试过程,并收集运行时覆盖文件以备日后分析。

我们的工具集是两个主要部分组成:自动执行此检测过程并提供了一个往返行程平台的我们构建了我们检测 ; IL 同步护盖和封面的查看器,Windows Presentation Foundation (WPF) 应用程序提供一个熟悉直观地浏览覆盖文件的设置。

捕获争用

现在,您应知道我们如何可以观察和记录同步点时出现争用。 在"隐藏的分支"回顾心理模型可能是如何实现 Monitor.Enter 提供我们提示,如果我们应用它几乎是按其原义我们转换中。 我们可以看到前面的数值调节钮的锁代码简化实现中获取方法捕获此返回值中的精确信息。 我们要做的就是看到它。

根据我们想要重写其他人的 IL,插入任意分支承诺的问题,而不是,我们将避免打开 Pandora 的。 撤回录制和链接同步点数据的第二个问题建议,我们转到我们的程序员工具框,并达到某些间接寻址。

我们通过引入一个包装类--一个同步识别覆盖范围的表面,其方法采用一个 syncId 参数的监视器上解决了这两个问题。 此 syncId 是生成唯一地标识一个同步点的唯一整数--也就是我们给每个同步点一个唯一的 ID,而我们命中同步点时, 将该 ID 传入到我们的包装类。 我们的覆盖范围实现首先调用 Monitor.TryEnter,录制结果和必要,然后,将原始的阻塞 Monitor.Enter 委派。 同步点状态管理由简单的内存中的数据库类,我们调用该 CoverageTracker。 将所有部分组合在一起都放,Monitor.Enter 我们覆盖率版本如下,如图 3 的中所示。

图 3 Monitor.Enter 覆盖率版本

namespace System.Threading.SyncCover {
public static class CoverMonitor {
public static void Enter(object obj, int syncId) {
if (Monitor.TryEnter(obj)) {
coverageTracker.MarkPoint(syncId, false); // No Contention.
} else {
Monitor.Enter(obj);
coverageTracker.MarkPoint(syncId, true); // Contention
}
}
}
}

请注意以支持 Monitor.Enter.NET 3.5 鐗堟湰在于在图 3 的 代码。 即将推出的.NET 4 监视器类型包括更改,使其对异步异常具有复原能力--请参阅 blogs.msdn.com/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx。 添加支持的.NET 4 重载是只需重载包装在一个类似 fashion.Once 我们得到我们的方法在手动,在检测过程变得相当直接转发。 让我们看看 IL 所产生的.NET 3.5 C# 编译器为锁定的语句,如下所示:

IL_0001: ldfld object class ThreadSafeQueue'1<!T>::m_lock
IL_0002: call void [mscorlib]System.Threading.Monitor::Enter(object)

我们只需调用 Monitor.Enter 查找并替换它们我们 CoverMonitor.Enter,调用时没有忘记插入其他 syncId 参数在方法调用之前。 以下代码阐释了这种转换:

IL_0001: ldfld object class ThreadSafeQueue'1<!T>::m_lock
I_ADD01: ldc.i4 1 // sync id
IL_0002: call void System.Threading.Coverage.Monitor::Enter(object, int32)

作为此过程最后一部分,应谈论有点报告,什么类型的信息非常有用。 到目前为止,我们知道所标识的直接插入目标源代码的一个 syncId 同步点。 如果将是更有用源文件和行号的同步的每个点可以得到。 我们发现 ILDASM /LINENUM 选项提供我们的信息需要的程序数据库 (PDB) 中提取文件的源位置和行号。

当我们在检测过程中遇到了一个新的同步点时,我们生成下一个 syncId 中,并捕获此图中的上下文信息。 然后为文件末尾的检测发出这种映射下面的代码所示:

T|ThreadSafeQueue.exe
SP|
0|D:\ThreadSafeQueue\ThreadSafeQueue.cs|9|ThreadSafeQueue`1<T>|Enqueue
1|D:\ThreadSafeQueue\ThreadSafeQueue.cs|15|ThreadSafeQueue`1<T>|Dequeue

图 4 的运行示例覆盖率

~|B3|~
A|ThreadSafeQueue.exe
C|ThreadSafeQueue.exe
D|20090610'091754
T|demo
M|DWEEZIL
O|Microsoft Windows NT 6.1.7100.0
P|4
SP|
0|1|1
1|1|0
~|E|~

显示我的数据!

它的显示时间。 我们覆盖的二进制文件后,它是执行任意多次您希望该程序的简单问题。 这将产生运行时文件包含 的图 4 所示,在执行过程中收集覆盖范围度量标准。 从市场中其他覆盖率工具记录灵感,我们提供了一个简单基于 WPF 的可视化覆盖率结果查看器。

但让我们来单步执行后的一段时间。 我们知道我们的单线程测试用例将提供我们零百分比同步覆盖范围。 我们如何可以修复该测试,以便我们可能会导致争用? 图 5 显示应导致争用方法排入队列中的略有提高的测试。 在运行测试之后我们可以看到该的结果,如图 6 的 中所示。

此处我们看到给定的同步点,这三种可能的覆盖范围情况。 在此的示例中,我们看到排入队列出现争用的至少一个数,因此将显示在绿色。 出列,另一方面执行但不是做争用,黄色中所示。 我们还添加了计数,永远不会调用了一个新属性并显示为红色。

图 5 A 排入队列上导致争用的测试

void ThreadedQueueTest()
{
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
Thread t1 = new Thread(
() =>
{
for (int i = 0; i < 10000; i++)
{
q.Enqueue(i);
}
});
Thread t2 = new Thread(
() =>
{
for (int i = 0; i < 10000; i++)
{
q.Enqueue(i);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Assert.AreEqual(0, q.Dequeue());
}

使用同步覆盖率

因此时应我们使用同步覆盖? 像任何代码覆盖率跃点数,度量值进行了如何测试您的代码试图同步覆盖范围。 锁定应用程序中的任何排序意味着打算同时,访问您的代码和严重应想您的测试套件是否实际执行这些锁。 您的团队应旨在为 100%同步覆盖率,尤其是当您的应用程序需要大量并行运行。

尝试练习我们 preach,我们使用了在测试并行扩展到.NET Framework Framework的过程中此同步覆盖率工具。 它具有帮助我们查找测试漏洞和缺陷,此开发周期并我们希望继续使用向前将跃点数。 两个同步覆盖了帮助我们的情况是特别有趣的:

并发的不多线程的测试

同步范围找到的一些测试,我们认为正在运行的同时没有实际。 图 7 是类似于图 5 的下,但到队列项的每个线程排队只有 10 这一次一个简单的多线程的测试。

图 7 A 并发测试的可能不在并发之后所有的

void ThreadedQueueTest() {
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
Thread t1 = new Thread(
() => {
for (int i = 0; i < 10; i++) {
q.Enqueue(10);
}
});
Thread t2 = new Thread(
() => {
for (int i = 0; i < 10; i++) {
q.Enqueue(12);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}

只是因为我们开始两个线程,但是,并不意味着它们实际上运行并行。 我们找到的内容时这些类型的测试与每个线程通常非常短的执行时间将具有一个完全执行后,其他线程的线程。 我们想要的统一体验争用测试。 一种解决方案是每个线程排队,使其更有可能导致争用的其他项目。 更好的解决方案是使用类似 CHESS,强制每个胶片交错发生两个线程之间的一个工具。

不必要的锁定

则很可能一些同步点没有被涉及因为它们不能被覆盖。 在下面的代码示例,如果 b 该锁始终是时获取锁在一个被保留它是不可能涵盖 B: 锁定

lock(a) {
lock(b) {
//... stuff ...
}
}

令人惊讶的是,同步覆盖有实际帮助我们找到这些不必要的锁定。 事实证明有时会保护锁定的资源可能不再需要。 例如,竞争资源上的线程可能不是不再需要并从该的代码中删除但锁在其余的线程上未被删除。 无害的行为额外的锁时它仍可能会降低性能。 (请注意,但这只是因为锁永远不会在为争取并不意味着它并不必要的 ;这样的信息只提供起始点的调查以确定是否它的实际需要)。

限制和将来的工作

像任何代码覆盖率跃点数,同步范围并不完美--它有其本身的局限性。 主要的同步覆盖率共享用其他的覆盖范围度量标准它无法测量什么不是有限制。 同步范围不能告诉您您需要对某些资源的仅有被已锁定该资源争取通过锁定。 因此,甚至 100%同步覆盖范围不是说测试工作已完成,仅您已在您的测试实现某种级别的完整性。

另一个问题是检测您的代码覆盖率同步可能更改的日程排定中测试该线程,这样可能会覆盖您已检测的测试运行而不是您 uninstrumented 执行在 (尽管我们的经验中我们发现这通常并不是问题)。 再次,CHESS 类工具可以帮助。 我们的原型工具使我们能够通过监视器和 $ Interlocked 操作测量争用。 我们在将来计划添加功能,将使我们可以测量如信号量和 ManualResetEvent 其他同步基元。 我们相信同步覆盖率,像一个代码覆盖率指标可能为有用和广泛的并发的应用程序为语句覆盖率单线程应用程序。

度量必须

您不能提高您不能度量值。 因此,我们开发越来越多线程的软件应用程序,我们必须度量方式彻底我们已经过测试并发方面我们的软件。 同步范围是执行此操作的一种简单、 实用的方法。 我们希望当您浏览整个新世界上的多核计算,您可以采取该想法本文以提高您质量的过程中。

更多信息

  • 并行计算在 Microsoft 中:
    msdn.microsoft.com/concurrency
  • 同步覆盖率纸张:
    Bron,A.,Farchi,E.,Magid,Y.,Nir,Y.和 Ur,S。 2005. 同步覆盖范围的应用程序。 在程序的原则和练习的并行编程 (芝加哥,Ill.,美国,2005 年 6 月 15-17,),第十个 ACM SIGPLAN 座谈会。 PPoPP"5。 ACM,北京,N.Y.206-212。
  • CHESS 工具:
    research.microsoft.com/en-us/projects/chess/default.aspx
    Musuvathi M。 和 Qadeer S. 2007. 边界的多线程程序的系统测试的迭代上下文。 在 2007 ACM SIGPLAN 会议上程序设计语言和 (圣地亚哥,Calif.,美国,2007 年 6 月 10-13,) 实现的程序。 PLDI 07"。 ACM, New York, N.Y., 446-455。
  • 常见的编译器结构 (CCI):
    ccimetadata.codeplex.com

Roy Tan 与并行计算平台组在 Microsoft 的测试中的一个软件开发工程师。 他在接收到他的女士 在计算机科学位于弗吉尼亚州 2007年中的技术。
丽丽 Dern 在并发软件开发工程师并行计算平台团队在 Microsoft--在唯一更好地比编写并发的软件测试的测试中。