如果我有这样的东西(伪代码):
class A
{
List<SomeClass> list;
private void clearList()
{
list = new List<SomeClass>();
}
private void addElement()
{
list.Add(new SomeClass(...));
}
}
当两个函数并行执行时,我是否可能遇到多线程问题(或任何类型的意外行为)?
用例是一个错误列表,可以随时清除(通过简单地指定一个新的空列表)。
编辑:我的假设是
答案 0 :(得分:10)
这里有两种可能的问题:
AddElement
和ClearList
,则会出现竞争条件:元素将在新列表中结束,或者在旧的(遗忘的)列表中结束。List<T>
对于多线程变异不安全,因此如果两个不同的线程同时调用AddElement
,则无法保证结果鉴于您正在访问共享资源,我会在访问时亲自持有锁。您仍然需要考虑在添加项目之前/之后立即清除列表的可能性。
编辑:我的评论如果你只是从一个帖子中添加就没关系,这已经有点可疑了,原因有两个:List<T>
。我不确定,.NET 2.0内存模型(与ECMA规范中的模型相反)可能足以避免这种情况,但说它很棘手。list
变量的更改,仍然会添加到旧列表中。实际上,如果没有任何同步,它可以永远看到旧值当您在混合中添加“在GUI中迭代”时,它变得非常棘手 - 因为您在迭代时无法更改列表。对此最简单的解决方案可能是提供一种方法,该方法返回列表的副本,并且UI可以安全地迭代:
class A
{
private List<SomeClass> list;
private readonly object listLock = new object();
private void ClearList()
{
lock (listLock)
{
list = new List<SomeClass>();
}
}
private void AddElement()
{
lock (listLock)
{
list.Add(new SomeClass(...));
}
}
private List<SomeClass> CopyList()
{
lock (listLock)
{
return new List<SomeClass>(list);
}
}
}
答案 1 :(得分:2)
是的 - 有可能,。事实上,如果真的同时被调用,那很有可能。
此外,如果同时发生对addElement的两次单独调用,也可能会出现问题。
对于这种多线程,你真的需要在列表本身周围进行某种互斥锁定,因此一次只能调用基础列表上的一个操作。
围绕这个原油锁定策略会有所帮助。类似的东西:
class A
{
static object myLock = new object()
List<SomeClass> list;
private void clearList()
{
lock(myLock)
{
list = new List<SomeClass>();
}
}
private void addElement()
{
lock(myLock)
{
list.Add(new SomeClass(...));
}
}
}
答案 2 :(得分:2)
.NET中的集合(最多3.5个)不是线程安全的或非阻塞的(并行执行)。您应该通过从IList派生并使用ReaderWriterLockSlim来执行每个操作来实现您的。例如,您的Add方法应如下所示:
public void Add(T item)
{
_readerWriterLockSlim.EnterWriteLock();
try { _actualList.Add(item); }
finally { _readerWriterLockSlim.ExitWriteLock(); }
}
你必须知道一些并发技巧。例如,您必须有一个GetEnumerator,它将一个新实例作为IList返回;不是实际的清单。否则你会遇到问题;应该是这样的:
public IEnumerator<T> GetEnumerator()
{
List<T> localList;
_lock.EnterReadLock();
try { localList= new List<T>(_actualList); }
finally { _lock.ExitReadLock(); }
foreach (T item in localList) yield return item;
}
和
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
注意:实现线程安全或并行集合(实际上是每个其他类)时,请不要从类中导出,但要接口!因为总会出现与该类的内部结构相关的问题,或者某些非虚拟的方法,您必须隐藏它们等等。如果你必须这样做,请非常小心!
答案 3 :(得分:1)
如果要清除它,只需创建一个新列表就不是一件好事。
我假设您还在构造函数中分配了列表,因此您不会遇到空指针异常。
如果你清除并添加了元素,可以将它们添加到我认为没问题的旧列表中?但如果同时添加两个元素,则可能会遇到问题。
查看.Net 4新集合来处理多线程任务:)
<强>此外:强>
如果你使用.Net 4,请查看名称空间System.Collections.Concurrent。在那里你会发现:System.Collections.Concurrent.ConcurrentBag<T>
和许多其他很好的集合:)
你还应该注意,如果你不注意,锁可以显着降低性能。
答案 4 :(得分:1)
如果在多个线程中使用此类的一个实例,是的。你会遇到问题。 .Net框架(3.5及更低版本)中的所有集合都不是线程安全的。特别是当你开始更改集合而另一个线程正在迭代它时。
在多线程环境中使用锁定并提供“复制”集合,或者如果可以使用.Net 4.0,则使用新的并发集合。
答案 5 :(得分:0)
从您的问题的编辑中可以清楚地看出,您并不真正关心通常的罪魁祸首 - 实际上并没有同时调用同一对象的方法。
基本上,您在询问是否可以在从并行线程访问列表时为列表分配引用。
据我了解,它仍然会造成麻烦。这完全取决于如何在硬件级别上实现引用分配。更准确地说,这个操作是否是原子操作。
我认为,尽管存在很小的问题,但仍有机会,特别是在多处理器环境中,该进程将被损坏的引用,因为它在访问它时只是部分更新。