首页 > 解决方案 > 实例化新实例是否使所有代码线程安全?

问题描述

编辑代码以使其成为线程安全的帖子评论

请参阅最后的更新问题。


你能帮我理解这段代码是否是线程安全的或者如何使它成为线程安全的吗?

设置

我的系统有一个非常简单的类,称为 WorkItem。

public class WorkItem
{
    public int Id {get;set;}
    public string Name {get;set;}
    public DateTime DateCreated {get;set;}
    public IList<object> CalculatedValues {get;set;}    
}

有一个接口 ICalculator,它有一个接受工作项、执行计算并返回 true 的方法。

public interface ICalculator
{
    bool Calculate(WorkItem WorkItem);
}

假设我们有两个 ICalculator 实现。

public class BasicCalculator: ICalculator
{
    public bool Calculate(WorkItem WorkItem)
    {
        //calculate some value on the WorkItem and populate CalculatedValues property 
        return true;
    }
}

另一个计算器:

public class AnotherCalculator: ICalculator
{
    public bool Calculate(WorkItem WorkItem)
    {
        //calculate some value on the WorkItem and populate CalculatedValues property
        //some complex calculation on work item
        if (somevalue==0) return false;
        return true;
    }
}

有一个计算器处理程序类。它的职责是按顺序执行计算器。

public class CalculatorHandler
{
    public bool ExecuteAllCalculators(WorkItem task, ICalculator[] calculators)
    {
        bool final = true;
        //call all calculators in a loop
        foreach(var calculator in calculators)
        {
            var calculatedValue = calculator.Calculate(WorkItem);
            final = final && calculatedValue;           
        }
        
        return final;
    }   
}

最后,在我的客户端类中,我注入了与运行相关的 ICalculators[]。然后我实例化 ExecuteCalculators() 方法。

现在我有大量工作项,我想对它们执行计算,所以我创建了一个任务列表,其中每个任务负责实例化 CalculatorHandler 实例,然后获取一个工作项并通过执行 WaitAll() 来执行计算所有的任务,例如

public class Client
{
    private ICalculators[] _myCalculators;
    
    public Client(ICalculators[] calculators)
    {
        _myCalculators = calculators;   
    }
    
    public void ExecuteCalculators()
    {
        var list = new List<Task>();
        for(int i =0; i <10;i++)
        {
            Task task = new Task(() => 

                var handler = new CalculatorHandler();
            
                var WorkItem = new WorkItem(){
                    Id=i,
                    Name="TestTask",
                    DateCreated=DateTime.Now
                };

                var result = handler.ExecuteAllCalculators(WorkItem, _myCalculators);
            );
            list.Add(task);
        }
        
        Task.WaitAll(list);
    }
}

这是系统的简化版本。实际系统有一系列计算器,Calculators 和 CalculatorHandler 通过 IoC 等注入。

我的问题是 - 帮助我理解以下几点:

  1. 每个任务都会创建一个新的 CalculatorHandler 实例。这是否意味着 CalculatorHandler 中发生的任何事情都是线程安全的,因为它没有任何公共属性并且只是在计算器上循环?

  2. 计算器在所有任务之间共享,因为它们是 Client 类的成员变量,但它们被传递到为每个任务实例化的 CalculatorHandler 中。这是否意味着当所有任务运行时,由于创建了新的 CalculatorHandler 实例,因此计算器自动线程安全,我们不会遇到任何线程问题,例如死锁等?

  3. 您能否建议我如何使代码线程安全?是否最好将Func<'ICalculators>'[]传递给 Client 类,然后在每个任务中,我们可以执行 Func<'ICalculator'>() 然后将这些实例传递给那里的 ICalculator?Func<'ICalculator'> 将返回 ICalculator 的实例。

  4. 计算器是否作为私有方法变量传入,因此 CalulatorHandler 的其他实例不能运行相同的计算器实例?还是因为计算器是引用类型,我们必然会遇到多线程问题?


更新

您能否帮助我了解此更新后的代码是否是线程安全的,或者如何使其成为线程安全的?

设置

我的系统有一个非常简单的类,称为 WorkItem。除了 1 个属性“CalculatedValues”外,它具有 getter 公共属性。

public class WorkItem
{
    public int Id {get;}
    public string Name {get;}
    public DateTime DateCreated {get;}
    public IList<object> CalculatedValues {get;set;}    
    public WorkItem(int id, string name, DateTime dateCreated)
    {
       Id = id,
       Name = name,
       DateCreated = dateCreated
    }
}

有一个接口 ICalculator,它有一个获取工作项、执行计算并返回 IList 的方法。它不会改变工作项的状态。

public interface ICalculator
{
    IList<object> Calculate(WorkItem WorkItem);
}

假设我们有两个 ICalculator 实现。

public class BasicCalculator: ICalculator
{
    public IList<object>Calculate(WorkItem WorkItem)
    {
        //calculate some value and return List<object>
        return List<object>{"A", 1};
    }
}

另一个计算器:

public class AnotherCalculator: ICalculator
{
    public bool Calculate(WorkItem WorkItem)
    {
        //calculate some value and return List<object>
        return List<object>{"A", 1, workItem.Name};
    }
}

有一个计算器处理程序类。它的职责是按顺序执行计算器。注意,它在实例化时在其构造函数中接受 ICalculators。当它更新工作项实例时,它也有一个私有静态锁对象。

public class CalculatorHandler
{
    private ICalculators[] _calculators;
    public CalculatorHandler(ICalculators[] calculators)
    {
         _calculators = calculators;
    }

    //static lock
    private static object _lock = new object();


    public bool ExecuteAllCalculators(WorkItem workItem, ICalculator[] calculators)
    {
        bool final = true;
        //call all calculators in a loop
        foreach(var calculator in calculators)
        {
            var calculatedValues = calculator.Calculate(workItem);

            //within a lock, work item is updated
            lock(_lock)
            {
               workItem.CalculatedValues = calculatedValues;
            }                           
        }
        
        return final;
    }   
}

最后,在我的客户端类中,我执行 CalculatorHandler。

现在我有大量工作项,我想对它们执行计算,所以我创建了一个任务列表,其中每个任务负责实例化 CalculatorHandler 实例,然后获取一个工作项并通过执行 WaitAll() 来执行计算所有的任务,例如

public class Client
{
    
    public void ExecuteCalculators()
    {
        var list = new List<Task>();
        for(int i =0; i <10;i++)
        {
            Task task = new Task(() => 
                
                //new handler instance and new calculator instances
                var handler = new CalculatorHandler(new[]{
                  new BasicCalculator(), new AnotherCalculator()
                });
            
                var WorkItem = new WorkItem(
                    i,
                    "TestTask",
                    DateTime.Now
                };

                var result = handler.ExecuteAllCalculators(WorkItem);
            );
            list.Add(task);
        }
        
        Task.WaitAll(list);
    }
}

这是系统的简化版本。实际系统有一系列计算器,Calculators 和 CalculatorHandler 通过 IoC 等注入。

我的问题是 - 帮助我理解以下几点:

  1. 每个任务都会创建一个新的 CalculatorHandler 实例和一个新的 ICalculators 实例。计算器不执行任何 I/O 操作,只创建一个新的私有 IList。计算器处理程序和计算器实例现在是线程安全的吗?

  2. CalculatorHandler 更新工作项但在锁定内。锁是一个静态私有对象。这是否意味着 CalculatorHandler 的所有实例都将共享一个锁,因此在某一时刻,只有一个线程可以更新工作项?

  3. 工作项具有所有公共 getter 属性,但其 CalculatedValues 属性除外。CalculatedValues 仅在静态锁内设置。这段代码现在是线程安全的吗?

标签: c#.netmultithreadingthread-safetytask-parallel-library

解决方案


1) 创建一个类的新实例,即使是没有公共属性的实例也不提供任何线程安全保证。问题是 ExecuteAllCalculators 需要两个对象参数。WorkItem 对象包含可变属性,并且相同的 WorkItem 对象用于所有 ICalculator 调用。假设其中一个计算器决定在 WorkItem.CalculatedValues 上调用 Clear()。或者假设一个计算器将 WorkItem.Name 设置为 null,而下一个计算器决定执行 WorkItem.Name.Length。这在技术上不是“线程”问题,因为这些问题可能在不涉及多个线程的情况下发生。

2) 跨线程共享的计算器对象绝对不是线程安全的。假设其中一个计算器实例使用类级变量。除非该变量以某种方式受到线程保护(例如:lock {...}),否则可能会产生不一致的结果。根据计算器实例的实现者的“创造性”程度,可能会出现死锁。

3) 每当您的代码接受接口时,您就是在邀请人们“在您的沙盒中玩耍”。它允许执行您几乎无法控制的代码。处理此问题的最佳方法之一是使用不可变对象。不幸的是,您不能在不破坏接口契约的情况下更改 WorkItem 定义。

4) 计算器通过引用传递。该代码显示 _myCalculators 在创建的所有任务之间共享。这并不能保证您会遇到问题,它只会让您可能会遇到问题。


推荐阅读