List<T>属性的线程安全访问

3
我在想这个语句是否会导致同步问题:

List<Character> characters = World.CharacterManager.Characters;

'Characters'是一个类

'CharacterManager.Characters'的结构可能是这样的:

public List<Character> Characters
{
    get
    {
        lock (this.objLock) { return this.characters; }
    }
}

这会导致同步问题吗?

我想使用引用的List迭代查找我要查找的字符。


1
我们需要更多的上下文,你是否需要进行锁定?您是否使用多个线程?您现在使用的那个锁并没有实现任何功能。 - Gary
3
我认为可以安全地假设多线程,因为他不会在单线程环境中询问同步问题... - Phil
谢谢Phil。是的,我可以确认这种情况是针对多线程应用程序的。 - TheAJ
这个“线程安全”的东西是什么?如果您不定义实际执行的操作以及期望它如何行为,那么我们就无法确定它是否能够满足您的期望。 - Servy
@Servy,请仔细看我的问题,我已经解释了我使用它的原因...此外,这个问题早就被回答了。 - TheAJ
4个回答

9
问题在于你正在get期间进行锁定,但一旦每个线程都拥有对集合的引用,它们可以同时对其进行操作。由于List<T>的成员不是线程安全的,因此在迭代、添加、删除等操作集合时会遇到随机错误和异常。

你可能需要返回一个线程安全的集合。没有100%兼容的线程安全版本,因此你需要查看System.Collections.Concurrent并找到一个可用的版本。


2

那种锁是无用的。你需要使用像Will建议的线程安全集合,或者如果你不需要写入访问,可以仅公开只读版本的列表,如下所示:

public ReadOnlyCollection<Character> Characters {
  get {
    lock (locker) { return this.characters.AsReadOnly(); }
  }
}

这些集合是不可修改的,如果您的Character类型是不可变的,则不会有任何同步问题。如果Character是可变的,则再次出现问题,但即使使用线程安全的集合也会出现这个问题。希望您知道这一点。您还可以公开返回IList<Character>的属性,但通常我认为告诉调用者对象是只读的更好。
如果您需要写访问权限,您也可以通过提供适当的方法在CharacterManager范围内进行同步来实现。Jesse编写了一个很好的示例。
编辑:ICollection<T>上没有SyncRoot。

请注意,List<T> 仅显式实现了 SyncRoot,因此必须将其强制转换为 ICollection 才能访问。 (参考:https://dev59.com/j2855IYBdhLWcg3w75Eu#4067371) - Jesse C. Slicer
谢谢!我还搜索并发现他们很久以前就已经弃用了这种模式。我已经编辑了答案。 - Andreas
谢谢,如果我调用类似这样的东西:World.CharacterManager.Characters.Count,它仍然是一个无用的锁吗? - TheAJ
1
你所声明的锁:是的!编译器甚至可能会将该锁移除,因为在将返回语句移到外部后它变为空。但是,在返回AsReadOnly()方法的结果的情况下,锁是有意义的,因为该方法中可能会发生复杂的操作。 - Andreas

1

调用代码是否实际需要能够添加和删除列表中的内容?如果是,那就不被视为最佳实践。这里提供一种(可能的)实现方式,而无需该要求,而是将Character项的添加和删除放入CharacterManager类本身:

internal sealed class CharacterManager
{
    private readonly IList<Character> characters = new List<Character>();

    public ReadOnlyCollection<Character> Characters
    {
        get
        {
            lock (this.characters)
            {
                return this.characters.AsReadOnly();
            }
        }
    }

    public void Add(Character character)
    {
        lock (this.characters)
        {
            this.characters.Add(character);
        }
    }

    public void Remove(Character character)
    {
        lock (this.characters)
        {
            this.characters.Remove(character);
        }
    }
}

你比我快了几秒钟,打败了我 :-) - Andreas
你提到基础“Character”类的可变性问题,因此可以获得+1分。 - Jesse C. Slicer

0

如果您只想让调用者枚举列表,则您的属性应该具有类型IEnumerable。如果是这种情况,那么我还会复制该列表并返回副本。如果在枚举列表时更改了列表,则它将变为无效并引发异常。权衡是调用者可能没有最新版本的列表。我倾向于将其转换为名为GetCharactersAsOfNow()的方法,而不是属性,以帮助显示每次调用时调用者需要获取更新的列表。

但是,如果您计划允许调用者修改列表,则必须注意列表不是线程安全的,并要求调用者执行线程同步。鉴于调用者现在有这个责任,因此您不再需要在属性getter中使用锁定。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接