在多线程应用程序中交换变量引用时是否需要锁定?

问题描述 投票:0回答:5

我有一个应用程序,我希望多个线程读取列表。我想定期用新数据更新列表。当列表更新时,我想我可以创建一个新列表并将其替换为旧列表。示例:

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
引用与新列表交换。根据我的想法,任何现有线程仍将保留对旧列表的引用(这对我来说没问题),任何新线程都将获取新列表。最终,所有线程都将结束,旧列表最终将被垃圾收集。这段代码中是否需要任何锁定,或者我需要注意什么以确保安全的多线程访问?

c# .net multithreading thread-safety
5个回答
9
投票

为了确保过时或优化都不会伤害您,请使用

Interlocked.Exchange()
更新字段。然后,您将在写入时拥有适当的内存屏障,而无需在每次读取时花费
volatile


4
投票

您正在使列表的实例不可变,只有可变状态才会导致问题。

您的代码很好,但您应该考虑将

_list
标记为
volatile
。这意味着所有读取都会获得最新版本,并且编译器/抖动/CPU 不会优化任何读取。

或者,您可以在分配新列表之前放置一个

Thread.MemoryBarrier()
,以确保在发布新列表之前已提交对新列表的所有写入,但这在 x86 架构上不是问题。


3
投票

赋值是原子的,你的代码没问题。您可能希望考虑将

_list
标记为易失性,以确保请求该变量的任何线程都获得最新版本:

private volatile List<string> _list = new List<string>();

3
投票

您需要阅读.net内存模型,因为通常处理器上没有定义权限的顺序,因此_list的新值可能会被写入共享内存,而新创建的对象的一部分由_list 仍在处理器写入队列中。

重新排序权限是一件令人痛苦的事情,所以我倾向于插入一个 Thread.MemoryBarrier();像这样。

private void UpdateList() 
{     
    var newList = new List<string>(QueryList(...));   
    Thread.MemoryBarrier();  
    _list = newList; 
} 

请参阅 Joseph Albahari 的 C# 中的线程网页了解更多详细信息

但是,我认为在大多数“普通”处理器上,您的代码将按编写的方式工作。


3
投票

如果您使用 .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 个线程,所有线程都在同一个列表上运行...

使用并发集合中隐含的不同行为可能会消除您拥有两个列表的需要。

© www.soinside.com 2019 - 2024. All rights reserved.