【问题标题】:Why is this boolean not thread-safe?为什么这个布尔值不是线程安全的?
【发布时间】:2018-07-04 15:15:21
【问题描述】:

我为集成测试编写了下面的代码,它为几个不同的程序集调用昂贵的函数 DoSomething()。我原以为应用锁足以确保线程安全,但有时布尔值在我当前的测试用例中应该始终为假时为真。

我还尝试了带有 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;
    }
}

【问题讨论】:

  • 你能把它变成一个可验证的失败例子吗?
  • 这不只是让您获得IsSuccesfullast 值吗,并且当您并行运行多个任务时,您不知道哪个是最后一个运行?
  • 不确定您要在这里做什么。如果您只想知道其中是否有任何失败,为什么不在任务失败时设置Failed 属性?
  • @stuartd 你能解释一下你的意思吗?我认为这是一个实现:我根据任务结果设置的布尔值。

标签: c# multithreading thread-safety


【解决方案1】:

你说DoSomething() 很贵(所以我猜耗时)。

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

  1. A 检查IsSuccessful,即true,因此调用DoSomething
  2. B 检查IsSuccessful 仍然是true,因此调用DoSomething
  3. DoSomething 为 B 返回 false(嗯,它恰好快一点),所以 B 将 IsSuccessful 设置为 false
  4. 现在DoSomething 为A 返回true。现在A 有true & true(因为IsSuccessful true 在读取它时),因此将IsSuccessful 设置为@987654341再次@。

您面临的问题是因为您使用不同的方法来检查值和设置值。这是经典的TOCTTOU problem

我建议使用Interlocked.Increment,但在Parallel.ForEach 内使用单个int

public bool Get()
{
    int failed = 0;
    Parallel.ForEach(..., ... =>
    {   
        if (!DoSomething())         
            Interlocked.Increment(ref failed);
    });

    return failed == 0;
}

如果您只对一个测试失败感兴趣,您当然可以简单地这样做:

public bool Get()
{
    Parallel.ForEach(..., ... =>
    {   
        if (!DoSomething()) IsSuccessful = false;
    });

    return IsSuccessful;
}

【讨论】:

  • 也许我没有关注,但我需要 10 个单独测试的结果,因为我需要知道其中一个是否失败,但还需要运行所有测试以获取额外的调试信息。所以我必须在 Parallel.ForEach() 之外创建变量,因为我必须在所有测试完成后返回它,而不是在测试失败时停止。
  • @bdebaere jsut 编辑了答案。但请注意,即使您的代码正常工作,当IsSuccessful 为假一次时,您也不会再调用DoSomething,因为当左侧已经为假时,&& 不会计算右侧操作数。
  • 但是单独的& 并不能解决你的线程安全问题:你读取 IsSuccessful,然后执行 DoSomething,然后& 之前读取的值和返回值,并再次设置这个结果到IsSuccessful。如果另一个线程在第一个线程运行 DoSomething 时更改了 IsSuccessful,您现在再次覆盖此更改。
  • 这是否意味着您只是解决了它,因为您增加了一个整数值,而不是根据该布尔值的读取值设置一个布尔值?
  • 不,这意味着我通过同时执行 checkwrite 解决了它,而您在写入之前很久就检查了值.您的线程安全 bool 类仅确保两个线程不能同时读取和设置值,但是您使用它的方式是错误的。您读取了该值,然后进行了长时间操作(在此过程中,所有其他线程都可以更改该值),然后根据很久以前的状态再次设置该值,而无需检查其当前状态。所以你的“线程安全”是没用的。
【解决方案2】:

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。这也可能使未来使用它的开发人员更清楚代码的意图。

【讨论】:

    猜你喜欢
    • 2015-07-24
    • 1970-01-01
    • 1970-01-01
    • 2018-11-07
    • 1970-01-01
    • 1970-01-01
    • 2011-09-25
    • 2014-06-22
    相关资源
    最近更新 更多