首先应该提到的是Property1
,在您的示例中,Property2
和Property3
在技术上称为字段,而不是属性。
在操作成功完成TestResult
后,您的示例在实例的完整性方面是完全安全的。Parallel.Invoke
它的所有字段都将被初始化,并且它们的值将对当前线程可见(但不一定对在完成之前已经运行的其他线程可见Parallel.Invoke
)。
另一方面,如果Parallel.Invoke
失败,则TestResult
实例可能最终被部分初始化。
如果Property1
,Property2
和Property3
实际上是properties,那么代码的线程安全性将取决于在set
这些属性的访问器后面运行的代码。如果此代码是微不足道的,例如set { _property1 = value; }
,那么您的代码将是安全的。
作为旁注,建议您Parallel.Invoke
使用合理的MaxDegreeOfParallelism
. 否则,您将获得Parallel
该类的默认行为,即使ThreadPool
.
TestResult testResult = new();
Parallel.Invoke(new ParallelOptions()
{ MaxDegreeOfParallelism = Environment.ProcessorCount },
() => testResult.Property1 = GetProperty1Value(),
() => testResult.Property2 = GetProperty2Value(),
() => testResult.Property3 = GetProperty3Value()
);
替代方案:如果您想知道如何在TestResult
不依赖闭包和副作用的情况下初始化实例,这是一种方法:
var taskFactory = new TaskFactory(new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default, Environment.ProcessorCount).ConcurrentScheduler);
var task1 = taskFactory.StartNew(() => GetProperty1Value());
var task2 = taskFactory.StartNew(() => GetProperty2Value());
var task3 = taskFactory.StartNew(() => GetProperty3Value());
Task.WaitAll(task1, task2, task3);
TestResult testResult = new()
{
Property1 = task1.Result,
Property2 = task2.Result,
Property3 = task3.Result,
};
属性的值临时存储在各个Task
对象中,最后在所有任务完成后,在当前线程上将它们分配给属性。TestResult
因此,这种方法消除了有关构造实例完整性的所有线程安全考虑。
但是有一个缺点:它Parallel.Invoke
利用了当前线程,并且也调用了它的一些动作。相反,这种Task.WaitAll
方法会浪费地阻塞当前线程,让线程ThreadPool
完成所有工作。
只是为了好玩:我尝试编写一个ObjectInitializer
工具,它应该能够并行计算对象的属性,然后按顺序(线程安全地)分配每个属性的值,而不必手动管理一堆分散的Task
变量。这是我想出的API:
var initializer = new ObjectInitializer<TestResult>();
initializer.Add(() => GetProperty1Value(), (x, v) => x.Property1 = v);
initializer.Add(() => GetProperty2Value(), (x, v) => x.Property2 = v);
initializer.Add(() => GetProperty3Value(), (x, v) => x.Property3 = v);
TestResult testResult = initializer.RunParallel(degreeOfParallelism: 2);
不是很漂亮,但至少它是简洁的。该Add
方法为一个属性添加元数据,并RunParallel
执行并行和顺序工作。这是实现:
public class ObjectInitializer<TObject> where TObject : new()
{
private readonly List<Func<Action<TObject>>> _functions = new();
public void Add<TProperty>(Func<TProperty> calculate,
Action<TObject, TProperty> update)
{
_functions.Add(() =>
{
TProperty value = calculate();
return source => update(source, value);
});
}
public TObject RunParallel(int degreeOfParallelism)
{
TObject instance = new();
_functions
.AsParallel()
.AsOrdered()
.WithDegreeOfParallelism(degreeOfParallelism)
.Select(func => func())
.ToList()
.ForEach(action => action(instance));
return instance;
}
}
它使用PLINQ而不是Parallel
类。
我会用它吗?可能不是。主要是因为并行初始化对象的需求并不经常出现,并且在这种罕见的情况下必须维护如此晦涩的代码似乎有点过头了。我可能会改用肮脏和副作用的Parallel.Invoke
方法。:-)