获得锁后枚举器线程安全吗?

本文关键字:线程 安全 枚举 | 更新日期: 2023-09-27 18:13:23

我想知道返回的枚举数是否线程安全:

public IEnumerator<T> GetEnumerator()
{
    lock (_sync) {
        return _list.GetEnumerator();
    }
}

如果我有多个线程正在向这个列表添加数据(也在lock()块内),一个线程枚举这个列表的内容。当枚举线程完成后,它会清除列表。那么使用从该方法获得的枚举数是否安全?

。枚举数是指向请求它的实例的列表副本,还是总是指向列表本身,而列表本身可能在枚举期间被另一个线程操作,也可能不被操作?

如果枚举数不是线程安全的,那么我能看到的唯一的其他操作过程是创建列表的副本并返回它。然而,这并不理想,因为它会产生大量的垃圾(这个方法每秒被调用大约60次)。

获得锁后枚举器线程安全吗?

不,一点也不。lock只同步对_list.GetEnumerator方法的访问;而列举一个列表远不止这些。它包括读取IEnumerator.Current属性,调用IEnumerator.MoveNext等。

您要么需要在foreach上加一个锁(我假设您通过foreach枚举),要么需要复制list。

更好的选择是看看现成的Threadsafe集合。

根据文档,为了保证线程安全,必须在整个迭代过程中锁定集合。

枚举数对集合没有独占访问权;因此,枚举整个集合本质上不是线程安全的过程。为了保证枚举期间的线程安全,可以在整个枚举过程中锁定集合。允许由多个线程访问的集合,用于读取和写,你必须实现你自己的同步。

另一个选择,可能是定义自己的自定义迭代器,并为每个线程创建一个新的实例。因此,每个线程都有自己的Current 只读指针的副本,指向同一个集合。

如果我有多个线程正在向这个列表添加数据(也在lock()块中),一个线程枚举这个列表的内容列表。当枚举线程完成后,它会清除列表。它会然后可以安全地使用从该方法获得的枚举数。

。参考:http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.aspx

只要该集合存在,枚举数就保持有效不变。如果对集合进行了更改,例如添加,修改或删除元素时,枚举数不可恢复无效,下次调用MoveNext或Reset会抛出InvalidOperationException。如果集合在MoveNext和Current, Current返回它被设置的元素,即使枚举数已经无效。枚举数没有对该集合具有独占访问权;因此,列举即使一个集合是同步的,其他线程仍然可以修改该集合导致枚举数抛出异常。来在枚举期间保证线程安全,您可以锁定在整个枚举期间捕获异常由其他线程所做的更改导致。

. .

枚举数是指向被请求实例的列表副本,还是总是指向列表本身可以或不可以被另一个线程在其枚举?

取决于集合。请参阅并发集合。Concurrent Stack, ConcurrentQueue, ConcurrentBag都在调用GetEnumerator()时获取集合的快照并从快照中返回元素。底层集合可以在不更改快照的情况下更改。另一方面,ConcurrentDictionary没有快照,因此在迭代时更改集合将立即根据上述规则产生影响。

在这种情况下,我有时使用的一个技巧是创建一个临时集合来迭代,这样当我使用快照时,原始集合是空闲的:
foreach(var item in items.ToList()) {
    //
}

如果您的列表太大,这会导致GC混乱,那么锁定可能是您最好的选择。如果锁太重,如果可行的话,可以考虑每个时间片进行部分迭代。

你说

:

当枚举线程结束时,它清除列表。

没有规定你必须一次处理整个列表。相反,您可以删除一系列项,将它们移动到单独的枚举线程中,让这个过程继续下去,然后重复。也许迭代和列表并不是最好的模型。考虑ConcurrentQueue,您可以使用它来构建生产者和消费者模型,消费者只是稳定地删除要处理的项目,而不需要迭代