静态列表多线程应用程序

问题描述 投票:0回答:4
public class Helper
{
         private static List<TimeZoneMap> timeZoneList = null;


public static List<TimeZoneMap> GetTimeZoneList()
 {
     if (timeZoneList == null)
     {
         timeZoneList = new List<TimeZoneMap>();
         timeZoneList.Add(new TimeZoneMap() { Id = "Dateline Standard Time", DisplayName = "(UTC-12:00) International Date Line West" });
         timeZoneList.Add(new TimeZoneMap() { Id = "UTC-11", DisplayName = "(UTC-11:00) Coordinated Universal Time-11" });
         timeZoneList.Add(new TimeZoneMap() { Id = "Aleutian Standard Time", DisplayName = "(UTC-10:00) Aleutian Islands" });}
}

timeZoneList
在实例类(Helper)内初始化,但它是静态列表。

静态方法根据空检查将项目添加到此列表。

在多线程环境中,

timeZoneList
不具有所有值并随机返回空值。在 200 个实例中,只有 2 到 3 个随机实例会出现这种情况。

我们在这里做错了什么?

  1. 静态列表
  2. 静态方法
  3. 静态列表中的实例类初始化?
  4. 或者从父类的不同实例调用相同的静态方法。 ?

这种情况并不经常发生,也无法猜测问题何时发生。

考虑将此列表作为静态只读集合。但仍然怀疑这会使列表只读。初始加载时我们只得到 5 个而不是全部!?

c# multithreading static instance object-reference
4个回答
3
投票

如果多个线程同时调用

GetTimeZoneList()
,而
timeZoneList
为空,则会出现竞争条件。它是静态的这一事实在这里无关紧要。

这样做的原因是,如果两个线程在列表为空时同时进入该方法,它们将同时进入

if
。这很糟糕,因为现在您可以并发写入
timeZoneList
字段、并发修改非线程安全集合,或者两者兼而有之。最终,您的问题是您在线程之间共享可变状态,这总是不好的。

还有另一个问题,即使用可变集合(

List<T>
)来处理可能在线程之间共享的内容,这会产生代码的某些部分随后修改它的风险,从而造成进一步的破坏。我建议使用类似
ImmutableArray<T>
的东西来代替:

var builder = ImmutableArray.CreateBuilder<TimeZoneMap>();
builder.Add(new TimeZoneMap() {
    Id = "Dateline Standard Time",
    DisplayName = "(UTC-12:00) International Date Line West" 
});
builder.Add(new TimeZoneMap() {
    Id = "UTC-11",
    DisplayName = "(UTC-11:00) Coordinated Universal Time-11" 
});
builder.Add(new TimeZoneMap() { 
    Id = "Aleutian Standard Time", 
    DisplayName = "(UTC-10:00) Aleutian Islands" 
});}
return builder.ToImmutable();

一旦构建了

ImmutableArray<T>
,就无法更改,从而防止出现其他问题。然后,您可以选择返回
IReadOnlyCollection<T>
IReadOnlyList<T>
以隐藏该实现细节。

让我们看看您的选择。

1.使其不可变

静态字段的初始化保证只发生一次,因此这本质上是线程安全的:

private static readonly ImmutableArray<TimeZoneMap> timeZoneList 
    = CreateTimeZoneList();

private static ImmutableArray<TimeZoneMap> CreateTimeZoneList()
{
    // create, fill and return the list
}

public static IReadOnlyList<TimeZoneMap> GetTimeZoneList() 
    => timeZoneList;

它确实存在使初始化急切的问题,这可能不是您想要的。为了保持懒惰,请继续阅读。

2.锁定它

使用带有对象的

lock
 语句来控制对该字段的访问:

private static ImmutableArray<TimeZoneMap> timeZoneList = null;
private static readonly object timeZoneLock = new object();

public static IReadOnlyList<TimeZoneMap> GetTimeZoneList()
{
    lock(timeZoneLock)
    {
        if (timeZoneList == null)
        {
            // etc.
        }
    }
    return timeZoneList;
}

这相当简单,但有点冗长。而且,看起来像单例,我不喜欢单例。您还可以在锁之外进行另一个空检查,以避免不必要地锁定它,但这是一种优化。由你决定。

使用
Lazy<T>

对我来说,这是最好的方法:它更简单、更精简、更健壮。

Lazy<T>
 是一个类,顾名思义,包含一个延迟初始化的值。默认情况下,该对象以线程安全的方式执行初始化(具体来说,使用
ExecutionAndPublication
线程安全模式),因此,您可以这样做:

private static readonly Lazy<ImmutableArray<TimeZoneMap>> timeZoneList 
    = new Lazy<ImmutableArray<TimeZoneMap>>(CreateTimeZoneList);

private static ImmutableArray<TimeZoneMap> CreateTimeZoneList()
{
    // ...
}

public static IReadOnlyList<TimeZoneMap> GetTimeZoneList() 
    => timeZoneList.Value;

上面急切地创建了惰性对象,指定用作其工厂的方法。该列表最初并未创建。当某些人第一次获得懒惰的

Value
(通过调用
GetTimeZoneList()
)时,它将被正确初始化。后续调用将返回相同的列表。如果多个线程同时这样做,工厂方法仍然只会被调用一次,然后所有线程都会获得相同的确切列表。


1
投票

这里需要单例模式。

目的是如果第一个到达的线程为空,则允许多个线程进入,从而锁定进程中的对象。随后到达的线程等待另一个线程。由于可以同时锁定,因此为此目的实施了额外的检查。

public class Helper
{
    private static List<TimeZoneMap> timeZoneList = null;
    private static readonly object lockObject = new object();

    public static List<TimeZoneMap> GetTimeZoneList()
    {
        // Double-check locking for optimization
        if (timeZoneList == null)
        {
            lock (lockObject)
            {
                if (timeZoneList == null)
                {
                    timeZoneList = new List<TimeZoneMap>
                    {
                        new TimeZoneMap { 
                          Id = "Dateline Standard Time",
                          DisplayName = "(UTC-12:00) International Date Line West" 
                        },
                        new TimeZoneMap { 
                          Id = "UTC-11", 
                          DisplayName = "(UTC-11:00) Coordinated Universal Time-11"
                        },
                        new TimeZoneMap { 
                          Id = "Aleutian Standard Time", 
                          DisplayName = "(UTC-10:00) Aleutian Islands"
                        }
                    };
                }
            }
        }

        return timeZoneList;
    }
}

0
投票

为什么不使用

Lazy<List<TimeZoneMap>>
而不是
List<TimeZoneMap>
?它本身可以负责初始化。


-2
投票

考虑到您的代码,我尝试了以下操作

class Program
    {
        public static void Main(string[] args)
        {
            int methodIterations = 200;

            Parallel.For(0, methodIterations, i =>
            {
                foreach(var z in Helper.GetTimeZoneList())
                {
                    Console.WriteLine(z.Id);
                }
            });
            Console.WriteLine("\n");
        }
    }

这模拟了多线程环境,并且您的方法中存在由于打字错误或错误而出现的错误,已更正如下

  public static List<TimeZoneMap> GetTimeZoneList()
        {
           
                if (timeZoneList == null)
                {
                    timeZoneList = new List<TimeZoneMap>
                    {
                        new TimeZoneMap { Id = "Dateline Standard Time", DisplayName = "(UTC-12:00) International Date Line West" },
                        new TimeZoneMap { Id = "UTC-11", DisplayName = "(UTC-11:00) Coordinated Universal Time-11" },
                        new TimeZoneMap { Id = "Aleutian Standard Time", DisplayName = "(UTC-10:00) Aleutian Islands" }
                      
                    };
                }
            

            return timeZoneList;
        }

每次调用该方法时,输出值都会打印在控制台中,因此除了我在方法中更正的内容之外[它没有返回值] 我没有做任何改变,但你提到的问题仍然不能重现。 您可以在这里添加您认为必要的更多详细信息。

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