尽管几十年来小型化的进步已经为单处理器带来了巨大的性能提升,但这个时代似乎即将结束。使用单芯片实现显着额外性能的最佳选择是通过多个内核,但前提是软件可以编程以利用它们。
不幸的是,并发编程很困难。即使是只熟悉单线程编程的专家级程序员也常常无法理解并发程序容易受到诸如竞争条件、死锁和饥饿等全新类别的缺陷的影响。人类很难推理并发程序,并且编程语言本身的某些方面不适合并发。因此,专家们经常偶然发现这些危害。以下讨论描述了常见的并发缺陷,并解释了静态分析工具如何在不执行程序的情况下发现此类缺陷。
竞争条件的后果
当多个执行线程访问一个共享的数据并且其中至少一个线程在没有显式同步操作来分离访问的情况下更改该数据的值时,就会出现竞争条件。根据两个线程的交错,系统可能会处于不一致的状态。
种族条件特别阴险,因为它们可以无限期地潜伏而未被发现,并且只在极少数情况下出现,表现出难以诊断和重现的神秘症状。特别是,它们很可能通过对已部署软件的测试而存活下来。充其量,这意味着增加开发时间;在最坏的情况下,后果可能是毁灭性的。
2003 年东北大停电如此普遍的一个原因是计算机化能源管理系统中的竞争条件导致向运营商传达误导性信息。正如 Kevin Poulsen 在 2004 年的一篇文章 ( www.securityfocus.com/news/8412 ) 中指出的那样,“该漏洞有一个以毫秒为单位的机会窗口。” 在测试过程中出现此类问题的可能性微乎其微。在另一种情况下,iOS 4.0 到 4.1(现已修复)中的竞争条件意味着任何可以物理访问 iPhone 3G 或更高版本的人都可以在某些条件下绕过其密码锁定。
图 1 显示了一个简单竞争条件的示例。带有入口和出口传感器的制造装配线维护当前生产线上的项目的运行计数。每次项目进入行时,此计数都会增加,每次项目到达行尾并退出时,此计数就会减少。如果一个项目在另一个项目退出的同时进入该行,则计数应该递增然后递减(或反之亦然),以使净变化为零。但是,正常的递增和递减不是原子操作;它们由一系列单独的指令组成,这些指令首先从内存中加载值,然后在本地对其进行修改,最后将其存储回内存中。如果更新事务是在没有足够保护措施的多线程系统中处理的,由于传感器读取和写入共享数据:计数,因此可能会出现竞争条件。图 1 中的交错导致错误计数为 69。也有可能导致错误计数为 71 的交错,以及一些正确导致计数为 70 的交错。
图 1:竞争条件导致装配线上的项目计数不正确。
对于这个例子和一般的竞争条件错误,标准调试技术可能由于几个原因而无效。
很少发生意味着发现问题的机会减少。如果问题不经常出现,它可能永远不会在测试期间出现。这个问题是双重的。首先,两个线程中可能的指令交错数量可能很大,并且随着指令数量的增加而急剧增加。这种现象被称为组合爆炸。如果线程 A 执行M条指令,线程 B 执行N条指令,则两个线程的可能交错为:
等式 1
例如,给定两个普通线程,每个线程有 10 条指令,则指令有 184,756 种可能的交错。现实世界的软件庞大而复杂;测试每一个交织是根本不可能的。其次,即使测试人员可以识别出一些值得检查的交错,也很难设置测试用例来确保它们确实发生,因为线程调度可能是高度不确定的。
如果详尽的测试难以解决,那么开发人员可以做什么?一种非常有用的方法是静态分析。CodeSonar 等高级静态分析工具使用高度复杂的符号执行技术同时考虑许多可能的执行路径和交错。这些技术可以在不需要执行程序的情况下找到竞争条件和其他并发错误。
有几个因素使比赛状况诊断变得困难。首先,症状可能令人困惑。在图 1 示例中,运行计数通常是正确的,但有时太高,有时太低。其次,不习惯考虑多线程编程的特定缺陷的程序员可能会在可能出现竞争条件之前花费大量时间对代码感到困惑。高级静态分析工具在这方面特别有用。他们通过检查共享内存位置的访问模式来识别竞争条件;也就是说,他们关注的是种族本身,而不是它的症状。当识别出竞争条件时,高级静态分析工具将报告它以及支持信息,以帮助用户进行评估和调试。程序员的负担大大减轻。
更复杂,更多错误
竞争条件通常通过使用锁来保护共享资源来避免。但是,锁可能会引入性能瓶颈,可能会阻止程序充分利用多核的潜力,因此程序员在使用它们时必须小心谨慎。编写有效使用锁的代码可能很棘手,这种复杂性可能导致一组不同的问题,即死锁和饥饿。
在死锁中,两个或多个线程相互阻止,因为每个线程都持有另一个线程需要的锁。图 2 显示了如何使用用于保护两个共享变量的两个锁出现死锁。在此示例中,多条装配线共享当前正在装配的项目总数,第二个 bad_items 值记录有多少成品未通过质量控制。一个线程在 count 上获得锁,另一个在 bad_items 上获得锁。两个线程都无法获得它需要的第二个锁;因此既不能执行它的操作,也不能到达释放锁的地步。由于两个更新都无法完成,因此两个线程都完全卡住了。
图 2:在两个线程之间的死锁中,两个线程都无法前进。
静态分析工具可以通过标记不同线程可以以不同顺序获取相同锁的情况来识别存在死锁风险的软件,例如图 2 中所示的线程。消除所有此类情况足以确保系统不会陷入死锁。
饥饿是使用锁的多线程程序中发生的另一个问题。如果一个线程正在等待另一个线程当前持有的资源需要很长时间,它可能会饿死。例如,假设上述制造自动化系统包括一个定期审核线程,该线程检查所有进入和退出记录,以确保运行计数与进入的总项目数相匹配,而不是退出的总项目数。审计线程需要锁定计数和所有传感器,因此所有更新都必须等待审计完成。如果审核运行很长时间,更新可能会显着延迟。如果运行时间过长,下一次审计可能会设法获取所有锁并在未完成的线程取得任何进展之前开始运行。在最坏的情况下,部分或全部更新可能永远没有机会运行。
静态分析可以通过提出诸如“在持有锁时是否调用长时间运行的库函数?”之类的问题来提供重要的价值。CodeSonar 等工具还为用户提供了添加自己检查的机制。如果已知内部函数 f() 具有较长的运行时间,工程师可以添加自定义检查,每当持有一个或多个锁的线程调用 f() 时触发警告。
多线程为嵌入式开发人员必须考虑的潜在错误添加了全新的类别,使得查找各种错误变得更加困难。最新一代的静态分析工具可以帮助解决这两个问题。
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !