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 个随机实例会出现这种情况。
我们在这里做错了什么?
这种情况并不经常发生,也无法猜测问题何时发生。
考虑将此列表作为静态只读集合。但仍然怀疑这会使列表只读。初始加载时我们只得到 5 个而不是全部!?
如果多个线程同时调用
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>
以隐藏该实现细节。
让我们看看您的选择。
静态字段的初始化保证只发生一次,因此这本质上是线程安全的:
private static readonly ImmutableArray<TimeZoneMap> timeZoneList
= CreateTimeZoneList();
private static ImmutableArray<TimeZoneMap> CreateTimeZoneList()
{
// create, fill and return the list
}
public static IReadOnlyList<TimeZoneMap> GetTimeZoneList()
=> timeZoneList;
它确实存在使初始化急切的问题,这可能不是您想要的。为了保持懒惰,请继续阅读。
使用带有对象的
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()
)时,它将被正确初始化。后续调用将返回相同的列表。如果多个线程同时这样做,工厂方法仍然只会被调用一次,然后所有线程都会获得相同的确切列表。
这里需要单例模式。
目的是如果第一个到达的线程为空,则允许多个线程进入,从而锁定进程中的对象。随后到达的线程等待另一个线程。由于可以同时锁定,因此为此目的实施了额外的检查。
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;
}
}
为什么不使用
Lazy<List<TimeZoneMap>>
而不是List<TimeZoneMap>
?它本身可以负责初始化。
考虑到您的代码,我尝试了以下操作
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;
}
每次调用该方法时,输出值都会打印在控制台中,因此除了我在方法中更正的内容之外[它没有返回值] 我没有做任何改变,但你提到的问题仍然不能重现。 您可以在这里添加您认为必要的更多详细信息。