EmptyEnumerable< T>.实例赋值和多线程设计
本文关键字:多线程 赋值 实例 EmptyEnumerable | 更新日期: 2023-09-27 18:15:30
我想这更像是一个设计问题,而不是一个真正的bug或抱怨。我想知道人们对以下行为的看法:
在。net中,当你想要有效地表示一个空的IEnumerable时,你可以使用Enumerable.Empty<MyType>()
,这将缓存空的enumerable实例。这是一个很好的免费的微优化,我想如果严重依赖的话,它可能会有所帮助。
然而,这是实现的样子:
public static IEnumerable<TResult> Empty<TResult>() {
return EmptyEnumerable<TResult>.Instance;
}
internal class EmptyEnumerable<TElement>
{
static volatile TElement[] instance;
public static IEnumerable<TElement> Instance {
get {
if (instance == null) instance = new TElement[0];
return instance;
}
}
}
我希望赋值发生在锁内,在另一次null检查之后,但事实并非如此。
我想知道这是一个有意识的决定(即,我们不关心潜在地创建几个对象,如果并发访问,我们会立即扔掉,因为我们宁愿避免锁定)还是只是无知?
你会怎么做?
这是安全的,因为volatile
对该字段的所有读写排序。在return instance;
的读操作之前,总是至少有一个写操作将该字段设置为有效值。
不清楚将返回什么值,因为这里可能会创建多个数组。但是总是会有一个非空数组
为什么这样做?好吧,锁的开销比volatile
大,而且实现起来很容易。这些额外的实例只会在多个线程竞争到这个方法时被创建几次。对于每个线程,最多只能创建一个实例。初始化完成后,没有垃圾。
注意,如果没有volatile,实例字段可以在被赋值后返回零。这是非常违反直觉的。如果没有任何同步,编译器可以这样重写代码:
var instanceRead1 = instance;
var returnValue;
if (instanceRead1 == null) {
returnValue = new TElement[0];
instance = returnValue;
}
var instanceRead2 = instance;
if (instanceRead2 == returnValue) return instanceRead2;
else return null;
在并发写的情况下,instanceRead2
可以是一个不同于刚写的值。没有编译器会做这样的重写,但这是合法的。CPU 可能在某些体系结构上做类似的事情。不太可能,但合法。也许有一个更合理的重写
在该代码中有可能创建多个数组。有可能让一个线程创建一个数组,然后最终实际使用从另一个线程创建的数组,或者两个不同的线程每个线程最终使用自己的数组。但是,在这里不重要。无论是否创建多个对象,代码都将正确工作。只要返回一个数组,任何调用返回哪个数组都无关紧要。此外,创建空数组的"开销"并不高。(很可能是在经过相当多的测试之后)决定,每次访问字段时同步访问字段的费用比创建两个额外的空数组的可能性要大得多。
这不是你应该在自己的(准)单例中模仿的模式,除非你也处于创建新实例很便宜的位置,并且创建多个实例不会影响代码的功能。实际上,只有当您试图缓存一个计算成本较低的操作的值时,这种方法才有效。这是一个微优化;这没有错,但也不是什么大胜利。
尽管在如此小的代码上运行基准测试并不能真正产生最可靠的结果,但这里有几个选项进行比较(非常坦率):
- 当前实现的
volatile instance
和null检查无锁。 锁定 -
typeof(T)
的锁(这样就不会创建静态类型初始化器)。
static object syncRoot
静态类型初始化器。结果(10亿次迭代的秒):
- 21.7 28.8 20.3 29.3
可以看到,lock
方法是目前为止最糟糕的方法。最好的是静态类型初始化器,它也会使代码更清晰。真正的原因可能不是锁,而是getter的大小,代码内联以及编译器优化代码的更多选项。
在同一台机器上创建100万个(不是10亿个)空数组的速度是26ms。
代码:
using System;
namespace ConsoleSandbox
{
class T1<T>
{
static volatile T[] _instance;
public static T[] Instance
{
get
{
if (_instance == null) _instance = new T[0];
return _instance;
}
}
}
class T2<T>
{
static T[] _instance;
static object _syncRoot = new object();
public static T[] Instance
{
get
{
if (_instance == null)
lock (_syncRoot)
if (_instance == null)
_instance = new T[0];
return _instance;
}
}
}
class T3<T>
{
static T[] _instance = new T[0];
public static T[] Instance
{
get
{
return _instance;
}
}
}
class T4<T>
{
static T[] _instance;
public static T[] Instance
{
get
{
if (_instance == null)
lock (typeof(T4<T>))
if (_instance == null)
_instance = new T[0];
return _instance;
}
}
}
class Program
{
static void Main(string[] args)
{
int[][] res = new int[2][];
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (var i = 0; i < 1000000000; i++)
res[i % 2] = T1<int>.Instance;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Restart();
for (var i = 0; i < 1000000000; i++)
res[i % 2] = T2<int>.Instance;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Restart();
for (var i = 0; i < 1000000000; i++)
res[i % 2] = T3<int>.Instance;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Restart();
for (var i = 0; i < 1000000000; i++)
res[i % 2] = T4<int>.Instance;
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Restart();
for (var i = 0; i < 1000000; i++)
res[i % 2] = new int[0];
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(res[0]);
Console.WriteLine(res[1]);
}
}
}