我有一个应用程序,我希望多个线程读取列表。我想定期用新数据更新列表。当列表更新时,我想我可以创建一个新列表并将其替换为旧列表。示例:
private List<string> _list = new List<string>();
private void UpdateList()
{
var newList = new List<string>(QueryList(...));
_list = newList;
}
private void ThreadRun()
{
foreach (var item in _list)
{
// process item...
}
}
在
UpdateList
方法中,创建一个新列表,并将 _list
引用与新列表交换。根据我的想法,任何现有线程仍将保留对旧列表的引用(这对我来说没问题),任何新线程都将获取新列表。最终,所有线程都将结束,旧列表最终将被垃圾收集。这段代码中是否需要任何锁定,或者我需要注意什么以确保安全的多线程访问?
为了确保过时或优化都不会伤害您,请使用
Interlocked.Exchange()
更新字段。然后,您将在写入时拥有适当的内存屏障,而无需在每次读取时花费 volatile
。
您正在使列表的实例不可变,只有可变状态才会导致问题。
您的代码很好,但您应该考虑将
_list
标记为 volatile
。这意味着所有读取都会获得最新版本,并且编译器/抖动/CPU 不会优化任何读取。
或者,您可以在分配新列表之前放置一个
Thread.MemoryBarrier()
,以确保在发布新列表之前已提交对新列表的所有写入,但这在 x86 架构上不是问题。
赋值是原子的,你的代码没问题。您可能希望考虑将
_list
标记为易失性,以确保请求该变量的任何线程都获得最新版本:
private volatile List<string> _list = new List<string>();
您需要阅读.net内存模型,因为通常处理器上没有定义权限的顺序,因此_list的新值可能会被写入共享内存,而新创建的对象的一部分由_list 仍在处理器写入队列中。
重新排序权限是一件令人痛苦的事情,所以我倾向于插入一个 Thread.MemoryBarrier();像这样。
private void UpdateList()
{
var newList = new List<string>(QueryList(...));
Thread.MemoryBarrier();
_list = newList;
}
请参阅 Joseph Albahari 的 C# 中的线程网页了解更多详细信息
但是,我认为在大多数“普通”处理器上,您的代码将按编写的方式工作。
如果您使用 .NET 4 或+,您可以使用新的线程安全集合...
BlockingCollection<string> list = new BlockingCollection<string>();
BlockingCollection 类具有用于添加和删除成员的线程安全方法,类似于生产者消费者设计模式。
线程可以添加列表成员,其他线程可以删除列表成员,而无需编程开销。
此外,它还允许你做这样的事情......
foreach (string i in list)
{
list.Take();
list.Add(i + 200);
}
此代码在枚举器工作时删除并添加到集合中,这是 .NET 4 之前的 C# 中永远无法完成的操作。无需额外将其声明为 volatile。
foreach (string i in list)
{
new Task(() =>list.Take()).Start();
new Task(() =>list.Add(i + 200)).Start();
}
在此片段中,启动了 N*2 个线程,所有线程都在同一个列表上运行...
使用并发集合中隐含的不同行为可能会消除您拥有两个列表的需要。