我为集成测试编写了以下代码,该集成测试为多个不同的程序集调用了昂贵的函数DoSomething()。我本以为应用锁足以确保线程安全,但是有时在我当前的测试用例中,布尔值始终为false时,布尔值为true。
我还尝试了Interlocked.CompareExchange解决方案here,但似乎对我也不起作用。
有人能指出我做错了什么正确的方向吗?
public class SomeClass
{
private object _lock = new object();
private volatile bool _isSuccessful = true;
private bool IsSuccesful
{
get
{
lock (_lock)
{
return _isSuccessful;
}
}
set
{
lock (_lock)
{
_isSuccessful = value;
}
}
}
public bool Get()
{
Parallel.ForEach(..., ... =>
{
IsSuccesful = IsSuccesful & DoSomething();
});
return IsSuccesful;
}
}
答案 0 :(得分:2)
您说过DoSomething()
很贵(所以我想这很费时间)。
现在想象两个并行调用,一个并行调用DoSometing()
成功(A),另一个并行失败(B)。显然这可能会发生:
IsSuccessful
的{{1}},因此称true
DoSomething
仍然是IsSuccessful
,所以称true
DoSomething
为B返回DoSomething
(嗯,碰巧快一点),因此B将false
设置为IsSuccessful
。false
返回A的DoSomething
。现在A具有true
(因为true & true
是 IsSuccessful
) ),因此再次将true
设置为IsSuccessful
。您面临的问题是因为您使用不同的方法来检查值和设置值。这是经典的TOCTTOU problem。
我建议使用Interlocked.Increment
,但要使用true
内的单个int
。
Parallel.ForEach
如果您只对其中一个 测试失败感兴趣,您当然可以简单地做到:
public bool Get()
{
int failed = 0;
Parallel.ForEach(..., ... =>
{
if (!DoSomething())
Interlocked.Increment(ref failed);
});
return failed == 0;
}
答案 1 :(得分:1)
volatile应该足够了。来自文档:
volatile修饰符通常用于由以下对象访问的字段 多个线程,而无需使用lock语句来序列化访问。
它有效地序列化了访问。
您说测试应始终返回false,但有时返回true。为了“知道”它总是返回false,那么所有的DoSomething()调用都必须返回false。真的吗?如果这是标准的(串行)foreach,则它将始终以相同的顺序执行,因此一旦对DoSomething的调用返回false _isSuccessful,则在其余的迭代中仍然为false。当您运行此并行时,订单不在窗口中。您最终得到的执行顺序可能每次都不同。您的DoSomething调用可能具有不同的完成时间,并且每次也可以以不同的顺序运行,这使情况更加复杂。您的结果将可能是不可重复且不一致的。
我假设您真正想要的是IsSuccessuful,仅当所有DoSomething返回true时,它才为true。一种方法是删除&&并使用简单的if语句。例如:
public bool Get()
{
var good = true;
Parallel.ForEach(..., ... =>
{
var result = DoSomething();
if (result == false)
{
good = false;
}
});
IsSuccesful = good;
return IsSuccesful;
}
一旦“好”变为假,就没有方法将其设置为真。因此,如果一个测试返回false,则IsSuccessful将返回false。这也可能使使用它的未来开发人员更加清楚代码的意图。