在 Parallel.Invoke 中更新同一对象的不同属性是线程安全的吗?

斯里达兰

我正在使用一个包含复杂属性的类。这些属性中的每一个都通过不同的方法计算。我正在使用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吗?

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

西奥多·祖利亚斯

首先应该提到的是Property1在您的示例中Property2Property3在技​​术上称为字段,而不是属性

TestResultParallel.Invoke操作成功完成后,您的示例对于实例的完整性是完全安全的它的所有字段都将被初始化,并且它们的值将由当前线程可见(但不一定对在 完成之前已经运行的其他线程可见Parallel.Invoke)。

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

如果Property1,Property2Property3实际上是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;
    }
    public TObject Run() => RunParallel(Environment.ProcessorCount);
}

它使用PLINQ而不是Parallel类。

我会用吗?可能不是。主要是因为并行初始化对象的需要并不经常出现,而且在这种罕见的情况下必须维护如此晦涩的代码似乎有点过分了。我可能会采用肮脏和副作用的Parallel.Invoke方法。:-)

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

Java中同一对象的不同实例上的多线程

使用Parallel.Invoke运行两个任务并添加超时,以防一个任务花费更长的时间

什么是C ++中的std :: invoke?

Parallel.Invoke和线程调用-澄清吗?

构造函数中的invoke-virtual与invoke-direct

了解Parallel.Invoke,创建和重用线程

收到错误消息“任务已取消。” 处理Parallel.Invoke中的大记录集时

投影函数对象上的invoke()

线程可以在Task.Factory.StartNew和Parallel.Invoke的不同处理器或内核上运行

Parallel.Invoke与Parallel.Foreach用于在大型列表上运行并行进程

Parallel.ForEach中每个线程的DbContext安全吗?

Ursina模块中invoke()的作用

当传递给invoke-command时,Powershell 7 ForEach-Object Parallel会中断自动变量

c#Parallel.Invoke(零速度改进)

同一对象的类型和实例的不同属性列表?

Parallel.Invoke(),TransactionScope()和SqlBulkCopy

Parallel.Invoke不等待异步方法完成

在SQL Server和Parallel.Invoke中编写查询

Parallel.Invoke仅执行第一个方法

Visual Studio 2010中Parallel.Invoke中的Uncatchable异常

Parallel.Invoke 和 Dispatcher.Invoke 之间的死锁

线程安全/Invoke 方法导致 StackOverflowException

从不同的线程调用 Form 方法(Invoke)

使用 tbb::parallel_invoke 时导致分段错误的原因是什么?

这个特定的 Parallel.For() 循环线程安全吗?

无法在 Invoke 中调用方法

在運行時添加和刪除 Parallel.Invoke 操作

Invoke-WebRequest 和 Invoke-RestMethod 的不同結果

tbb::parallel_invoke 只执行一次函数