理论上,至少可以通过暴力破解来验证无锁算法(函数调用的组合只有那么多种)。是否有工具或形式化推理过程可用于实际证明无锁算法的正确性(最好还能检查竞争条件和ABA问题)?
注意:如果您知道一种仅验证一点(例如仅验证其免受ABA问题的影响)或我未提及的问题的方法,则仍然可以发布解决方案。在最坏的情况下,每种方法都可以按顺序执行以完全验证它。
理论上,至少可以通过暴力破解来验证无锁算法(函数调用的组合只有那么多种)。是否有工具或形式化推理过程可用于实际证明无锁算法的正确性(最好还能检查竞争条件和ABA问题)?
注意:如果您知道一种仅验证一点(例如仅验证其免受ABA问题的影响)或我未提及的问题的方法,则仍然可以发布解决方案。在最坏的情况下,每种方法都可以按顺序执行以完全验证它。
您绝对应该尝试使用Spin模型检查器。
您可以使用一个名为Promela的简单类C语言编写类似程序的模型,Spin会将其内部转换为状态机。一个模型可以包含多个并行进程。
接下来Spin会检查每个进程中指令的所有可能交错组合,并根据您想测试的任何条件进行检查,通常是检查是否存在竞争条件或死锁等问题。大多数这样的测试都可以通过使用assert()
语句轻松编写。如果存在任何可能违反断言的执行序列,这些序列会被打印出来,否则您将收到“全清”的消息。
(实际上,它使用了更高级和更快速的算法来完成此操作,但这是效果。默认情况下,将检查所有可达的程序状态。)
这是一个令人难以置信的程序,它获得了2001年的ACM系统软件奖(其他获奖者包括Unix、Postscript、Apache和TeX等)。我很快就开始使用它,在几天内就能够在Promela中实现MPI函数MPI_Isend()
和MPI_Irecv()
的模型。Spin发现了我将并行代码转换到Promela进行测试时一个棘手的竞争条件。
Spin确实很优秀,但也要考虑Dmitriy V'jukov的Relacy Race Detector。它专门用于验证包括非阻塞(wait-/lock-free)算法在内的并发算法。它是开源的并且授权宽松。
Relacy提供POSIX和Windows同步原语(互斥量、条件变量、信号量、CriticalSection、win32事件、Interlocked*等),因此您的实际C++实现可以被馈送到Relacy进行验证。无需像Promela和Spin那样开发算法的单独模型。
Relacy提供C++0x std::atomic
(显式内存顺序胜出!),因此您可以使用预处理器#defines
来在Relacy的实现和您自己的平台特定原子实现(tbb::atomic、boost::atomic等)之间进行选择。
调度是可控制的:随机、上下文绑定和完全搜索(所有可能的交错)都可用。
这是一个Relacy程序示例。需要注意以下几点:
$
是Relacy宏,记录执行信息。rl::var<T>
标记“正常”(非原子)变量,这些变量也需要作为验证的一部分考虑。代码如下:
#include <relacy/relacy_std.hpp>
// template parameter '2' is number of threads
struct race_test : rl::test_suite<race_test, 2>
{
std::atomic<int> a;
rl::var<int> x;
// executed in single thread before main thread function
void before()
{
a($) = 0;
x($) = 0;
}
// main thread function
void thread(unsigned thread_index)
{
if (0 == thread_index)
{
x($) = 1;
a($).store(1, rl::memory_order_relaxed);
}
else
{
if (1 == a($).load(rl::memory_order_relaxed))
x($) = 2;
}
}
// executed in single thread after main thread function
void after()
{
}
// executed in single thread after every 'visible' action in main threads
// disallowed to modify any state
void invariant()
{
}
};
int main()
{
rl::simulate<race_test>();
}
使用普通编译器进行编译(Relacy是仅包含头文件的)并运行可执行文件:
结构体race_test 数据竞争 迭代:8
执行历史: [0] 0: 原子存储,值=0,(先前值=0),顺序=seq_cst,在race_test::before,test.cpp(14) [1] 0: 存储,值=0,在race_test::before,test.cpp(15) [2] 0: 存储,值=1,在race_test::thread,test.cpp(23) [3] 0: 原子存储,值=1,(先前值=0),顺序=relaxed,在race_test::thread,test.cpp(24) [4] 1: 原子加载,值=1,顺序=relaxed,在race_test::thread,test.cpp(28) [5] 1: 存储,值=0,在race_test::thread,test.cpp(29) [6] 1: 检测到数据竞争,在race_test::thread,test.cpp(29)
线程0: [0] 0: 原子存储,值=0,(先前值=0),顺序=seq_cst,在race_test::before,test.cpp(14) [1] 0: 存储,值=0,在race_test::before,test.cpp(15) [2] 0: 存储,值=1,在race_test::thread,test.cpp(23) [3] 0: 原子存储,值=1,(先前值=0),顺序=relaxed,在race_test::thread,test.cpp(24)
线程1: [4] 1: 原子加载,值=1,顺序=relaxed,在race_test::thread,test.cpp(28) [5] 1: 存储,值=0,在race_test::thread,test.cpp(29) [6] 1: 检测到数据竞争,在race_test::thread,test.cpp(29)
如果您需要Java和CLI内存模型的话,最近版本的Relacy也提供了这些。
我不知道你使用的平台或语言是什么 - 但在 .Net 平台上有一个名为 Chess 的微软研究项目,它在帮助我们做多线程组件(包括无锁)方面非常有前途。
我没有大量使用过它。
它的工作原理(简单解释)是通过明确地交错线程,以尽可能紧密的方式来强制将错误暴露到外部。 它还分析代码以查找常见的错误和不良模式 - 类似于代码分析。
过去,我还通过 #if 块等构建了特殊版本的相关代码,添加了额外的状态跟踪信息; 计数、版本等,然后可以在单元测试中进行调试。
问题在于编写代码需要更多的时间,并且您不能总是在不改变已有代码的基础结构的情况下添加此类内容。