实例化新实例会使所有代码线程安全吗?

时间:2018-07-03 22:52:03

标签: c# .net multithreading thread-safety task-parallel-library

编辑了代码,使其成为线程安全的帖子注释

请在最后查看更新的问题。


能否请您帮助我理解此代码是否是线程安全的或如何使其成为线程安全的?

设置

我的系统有一个非常简单的类,叫做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()方法。

现在我有大量的工作项,并且我想对它们执行计算,因此我创建了一个Task列表,其中每个任务都负责实例化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);
    }
}

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

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

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

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

  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。

现在我有大量的工作项,并且我想对它们执行计算,因此我创建了一个Task列表,其中每个任务都负责实例化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);
    }
}

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

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

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

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

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

2 个答案:

答案 0 :(得分:0)

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

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

3)每当您的代码接受接口时,您都在邀请人们“在您的沙箱中播放”。它允许执行您几乎无法控制的代码。处理此问题的最佳方法之一是使用不可变对象。不幸的是,您必须在不违反界面约定的情况下更改WorkItem定义。

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

答案 1 :(得分:0)

  1. 否,它不是线程安全的。如果任何计算中存在任何共享状态,则可能会出现线程问题。避免线程问题的唯一方法是确保不更新任何共享状态。这意味着只读对象和/或使用"pure" functions

  2. 您已经使用了“共享”一词-这意味着由于共享状态而不安全。除非您的意思是“分布式”而不是“共享”。

  3. 仅使用只读对象。

  4. 它们是引用类型,因此它们可以在单独的线程之间共享-因此不是线程安全的-除非它们是只读的。

这是一个只读对象的示例:

public sealed class WorkItem : IEquatable<WorkItem>
{
    private readonly int _id;
    private readonly string _name;
    private readonly DateTime _dateCreated;

    public int Id { get { return _id; } }
    public string Name { get { return _name; } }
    public DateTime DateCreated { get { return _dateCreated; } }

    public WorkItem(int id, string name, DateTime dateCreated)
    {
        _id = id;
        _name = name;
        _dateCreated = dateCreated;
    }

    public override bool Equals(object obj)
    {
        if (obj is WorkItem)
            return Equals((WorkItem)obj);
        return false;
    }

    public bool Equals(WorkItem obj)
    {
        if (obj == null) return false;
        if (!EqualityComparer<int>.Default.Equals(_id, obj._id)) return false;
        if (!EqualityComparer<string>.Default.Equals(_name, obj._name)) return false;
        if (!EqualityComparer<DateTime>.Default.Equals(_dateCreated, obj._dateCreated)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<int>.Default.GetHashCode(_id);
        hash ^= EqualityComparer<string>.Default.GetHashCode(_name);
        hash ^= EqualityComparer<DateTime>.Default.GetHashCode(_dateCreated);
        return hash;
    }

    public override string ToString()
    {
        return String.Format("{{ Id = {0}, Name = {1}, DateCreated = {2} }}", _id, _name, _dateCreated);
    }

    public static bool operator ==(WorkItem left, WorkItem right)
    {
        if (object.ReferenceEquals(left, null))
        {
            return object.ReferenceEquals(right, null);
        }

        return left.Equals(right);
    }

    public static bool operator !=(WorkItem left, WorkItem right)
    {
        return !(left == right);
    }
}

创建后就无法修改,因此线程安全不再是问题。

现在,如果我可以假设每个ICalculator也是在没有状态的情况下实现的,因此是纯函数,则该计算是线程安全的。但是,您的问题中没有什么让我知道我可以做这个假设的。因此,任何人都无法告诉您您的代码是线程安全的。

因此,在给定只读WorkItem和纯ICalculator函数的情况下,其余代码看起来就可以了。