为什么此布尔值不是线程安全的?

时间:2018-07-04 15:15:22

标签: c# multithreading thread-safety

我为集成测试编写了以下代码,该集成测试为多个不同的程序集调用了昂贵的函数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;
    }
}

2 个答案:

答案 0 :(得分:2)

您说过DoSomething()很贵(所以我想这很费时间)。

现在想象两个并行调用,一个并行调用DoSometing()成功(A),另一个并行失败(B)。显然这可能会发生:

  1. A检查IsSuccessful的{​​{1}},因此称true
  2. B检查DoSomething仍然是IsSuccessful,所以称true
  3. DoSomething为B返回DoSomething(嗯,碰巧快一点),因此B将false设置为IsSuccessful
  4. 现在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。这也可能使使用它的未来开发人员更加清楚代码的意图。