First and foremost it should be mentioned that the Property1, Property2 and Property3 in your example are technically called fields, not properties.
Your example is perfectly safe regarding the integrity of the TestResult instance, after the Parallel.Invoke operation has successfully completed.
All of its fields will be initialized, and their values will be visible by the current thread (but not necessarily visible by other threads that were already running before the completion of the Parallel.Invoke).
On the other hand if the Parallel.Invoke fails, then the TestResult instance may end up being partially initialized.
If the Property1, Property2 and Property3 were actually properties, then the thread-safety of your code would depend on the code running behind the set accessors of those properties. In case this code was trivial, like set { _property1 = value; }, then again your code would be safe.
As a side note, you are advised to configure the Parallel.Invoke operation with a reasonable MaxDegreeOfParallelism. Otherwise you'll get the default behavior of the Parallel class, which is to saturate the ThreadPool.
TestResult testResult = new();
Parallel.Invoke(new ParallelOptions()
{ MaxDegreeOfParallelism = Environment.ProcessorCount },
() => testResult.Property1 = GetProperty1Value(),
() => testResult.Property2 = GetProperty2Value(),
() => testResult.Property3 = GetProperty3Value()
);
Alternative: In case you are wondering how you could initialize a TestResult instance without relying on closures and side-effects, here is
one way to do it:
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,
};
The values of the properties are stored temporarily in the individual Task objects, and finally they are assigned to the properties, on the current thread, after the completion of all tasks. So this approach eliminates all thread-safety considerations regarding the integrity of the constructed TestResult instance.
But there is a disadvantage: The Parallel.Invoke utilizes the current thread, and invokes some of the actions on it too. On the contrary the Task.WaitAll approach will wastefully block the current thread, letting the ThreadPool do all the work.
Just for fun: I tried to write an ObjectInitializer tool that should be able to calculate the properties of an object in parallel, and then assign the value of each property sequentially (thread-safely), without having to manage manually a bunch of scattered Task variables. This is the API I came up with:
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);
Not very pretty, but at least it is concise. The Add method adds the metadata for one property, and the RunParallel does the parallel and sequential work. Here is the implementation:
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;
}
}
It uses PLINQ instead of the Parallel class.
Would I use it? Probably not. Mostly because the need for initializing an object in parallel doesn't come very often, and having to maintain so obscure code for such rare occasions seems like overkill. I would probably go with the dirty and side-effecty Parallel.Invoke approach instead. :-)