如果锁对象是锁中的最后一个语句,那么覆盖它是不是很糟糕吗?

时间:2017-10-20 16:33:09

标签: c# .net multithreading locking

我现在已经看过几次,但我不确定它是不是真的不对。

考虑以下示例类:

class Foo
{
    List<string> lockedList = new List<string>();

    public void ReplaceList(IEnumerable<string> items)
    {
        var newList = new List<string>(items);

        lock (lockedList) 
        {
            lockedList = newList;
        }
    }

    public void Add(string newItem)
    {
        lock (lockedList)
        {
            lockedList.Add(newItem);
        }
    }

    public void Contains(string item)
    {
        lock (lockedList)
        {
            lockedList.Contains(item);
        }
    }
}

ReplaceList会在锁定时覆盖lockedList。一旦所有后续调用者实际上将锁定新值。在ReplaceList退出锁定之前,他们可以进入锁定状态。

尽管通过替换锁定对象来提升标志,但这段代码实际上可能正常工作。只要赋值是锁的最后一个语句,就不再需要运行同步代码。

除了确保分配保持在锁定块结束时增加的维护成本之外,还有另一个原因可以避免这种情况吗?

2 个答案:

答案 0 :(得分:3)

因此,首先,由于您访问该字段的具体情况,您提供的具体解决方案并不安全。

获得可行的解决方案非常简单。只是不要创建一个新列表;相反,清除它并添加新项目:

class Foo
{
    private List<string> lockedList = new List<string>();

    public void ReplaceList(IEnumerable<string> items)
    {
        lock (lockedList)
        {
            lockedList.Clear();
            lockedList.AddRange(items);
        }
    }

    public void Add(string newItem)
    {
        lock (lockedList)
        {
            lockedList.Add(newItem);
        }
    }

    public void Contains(string item)
    {
        lock (lockedList)
        {
            lockedList.Contains(item);
        }
    }
}

现在你的领域实际上并没有改变,你也不必担心可能导致的所有问题。

至于问题中的代码如何破解,只需调用AddContains来读取字段,获取列表,锁定列表,然后再使用另一个线程替换该字段。当您第二次读取该字段时,在已经获得锁定值之后,该值可能已更改,因此您最终会变异或从列表中读取另一个呼叫者不会受到限制从访问。

所有这一切,虽然改变lockedList变量是一个真的坏主意,你应该毫无疑问地避免改变它,如上所示,你也可以确保你只是实际阅读该字段一次,而不是重复读取,并且您仍然确保每个列表只能在任何时间从单个线程访问:

class Foo
{
    private volatile List<string> lockedList = new List<string>();

    public void ReplaceList(IEnumerable<string> items)
    {
        lockedList = new List<string>(items);
    }

    public void Add(string newItem)
    {
        var localList = lockedList;
        lock (localList)
        {
            localList.Add(newItem);
        }
    }

    public void Contains(string item)
    {
        var localList = lockedList;
        lock (localList)
        {
            localList.Contains(item);
        }
    }
}

请注意,此修复的问题并不是要改变要锁定的对象的字段(这不是本身问题,尽管是非常不好的做法),而是从lock语句内部不断地从字段的所有用法中获取新值并期望该值永远不会改变,只有它可以。

这将更难维护,非常脆弱,并且更难以理解或确保正确性,所以再次做这样的事情。

答案 1 :(得分:2)

我意识到在锁定它之前读取锁定对象的字段已经完成了;所以这永远不会是正确的。如果另一个线程在更改值之前尝试进入锁定,它将最终进入其锁定块并锁定旧值,但该字段将具有新值。然后它将使用新值而不锁定它。

示例:

  1. 主题A调用ReplaceList并输入锁定。
  2. 主题B调用Add。它到达锁定并被阻止。
  3. 主题A用新值替换lockedList
  4. 线程C调用Contains,它获取lockedList的新值并取出锁定。
  5. 线程A退出其锁定,允许线程B恢复。
  6. 线程B使用旧值lockedList进入锁定,并将项目添加到新列表中,而不会锁定新列表。
  7. 线程C抛出异常,因为列表在枚举时已被修改。