正在并行更新同一对象的不同属性。调用是线程安全的吗? [英] Updating different properties of same object in Parallel.Invoke is thread-safe?

查看:30
本文介绍了正在并行更新同一对象的不同属性。调用是线程安全的吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用一个包含复杂属性的类。这些属性中的每一个都是通过不同的方法计算的。我使用Parallel.Invoke更新同一对象的不同属性。这是否会对对象造成任何问题?

// sample class definition. I've simplified the example by using 'object' type
// for complex types. 
public class TestResult
{
     public object Property1;

     public object Property2;

     public object Property3;
}

// here we populate an object. We are processing it parallelly because each method
// takes some considerable amount of time. 
var testResult = new TestResult();
Parallel.Invoke(
() =>
{
       testResult.Property1 = GetProperty1Value();
},
() =>
{
       testResult.Property2 = GetProperty2Value();
},
() =>
{
       testResult.Property3 = GetProperty3Value();
});

上述代码是否会对testResult对象造成任何问题?

注意:我已经测试了这部分代码。似乎不会引起任何问题。据我所知,由于不同的属性在不同的任务中进行处理,这应该不是问题。我找不到任何关于这个的文件。我想确认这一行为,因此提出这个问题。

推荐答案

首先值得一提的是,您示例中的Property1Property2Property3在技术上称为fields,而不是properties

Parallel.Invoke操作成功完成后,您的示例在TestResult实例的完整性方面是完全安全的。 它的所有字段都将由当前线程初始化,其值will be visible(但在Parallel.Invoke完成之前已运行的其他线程不一定可见)。

另一方面,如果Parallel.Invoke失败,则TestResult实例可能最终被部分初始化。

如果Property1Property2Property3实际上是properties,那么代码的线程安全性将取决于在这些属性的set访问器后面运行的代码。如果此代码很琐碎,如set { _property1 = value; },那么您的代码也将是安全的。

作为附注,建议您使用合理的MaxDegreeOfParallelism配置Parallel.Invoke操作。否则,您将获得Parallel类的默认行为,即saturate the 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变量。这是我想出来的接口:

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方法。:-)

这篇关于正在并行更新同一对象的不同属性。调用是线程安全的吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆