Делает ли создание нового экземпляра весь поток кода безопасным? - PullRequest
0 голосов
/ 04 июля 2018

Отредактировал код, чтобы сделать его потокобезопасным оставлять комментарии

Пожалуйста, посмотрите обновленный вопрос в конце.


Не могли бы вы помочь мне понять, является ли этот код потокобезопасным или как его можно сделать потокобезопасным?

Настройка

В моей системе есть очень простой класс 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);
    }
}

Это упрощенная версия системы. Актуальная система имеет ряд калькуляторов, калькуляторы и CalculatorHandler вводятся через IoC и т. Д.

Мои вопросы - помогите мне понять эти моменты:

  1. Каждая задача создает новый экземпляр CalculatorHandler. Является ли это означает, что все, что происходит в CalculatorHandler, является потокобезопасным, так как не имеет общедоступных свойств и просто зацикливается калькуляторы

  2. Калькуляторы используются всеми задачами, поскольку они являются переменными-членами класса Client, но передаются в CalculatorHandler, который создается для каждой задачи. Означает ли это, что когда все задачи запускаются, как новые экземпляр CalculatorHandler создан, поэтому калькуляторы автоматически безопасен для потоков, и мы не столкнемся с проблемами потоков, например, тупики и т.д?

  3. Подскажите, пожалуйста, как я могу сделать код безопасным? Это лучше всего передать Func <'ICalculators>' [] в класс Client, а затем в каждой задаче мы можем выполнить Func <'ICalculator'> () и затем передать эти экземпляры в ICalculator? Func <'ICalculator'> вернет экземпляр ICalculator.

  4. Правда ли, что калькуляторы передаются как переменная частного метода, поэтому другие экземпляры CalulatorHandler не могут запускать один и тот же экземпляр калькулятора? Или, поскольку калькуляторы являются ссылочными типами, мы неизбежно столкнемся с многопоточностью?


Обновление

Не могли бы вы помочь мне понять, является ли этот обновленный код поточно-ориентированным или как его можно сделать поточно-безопасным?

Настройка

В моей системе очень простой класс WorkItem. Он имеет общедоступные свойства геттера, кроме 1 свойства "CalculatedValues".

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};
    }
}

Есть класс обработчика калькулятора. В его обязанности входит выполнение калькуляторов последовательно. Обратите внимание, что он принимает ICalculator в своем конструкторе, когда он создается. Он также имеет частный объект статической блокировки при обновлении экземпляра рабочего элемента.

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);
    }
}

Это упрощенная версия системы. Актуальная система имеет ряд калькуляторов, калькуляторы и CalculatorHandler вводятся через IoC и т. Д.

Мои вопросы - помогите мне понять эти моменты:

  1. Каждая задача создает новый экземпляр CalculatorHandler и новые экземпляры ICalculators. Калькуляторы не выполняют никаких операций ввода-вывода, а только создают новый частный IList. Являются ли обработчики калькулятора и экземпляры калькулятора потокобезопасными?

  2. CalculatorHandler обновляет рабочий элемент, но в пределах блокировки. Блокировка - это статический закрытый объект. Означает ли это, что все экземпляры CalculatorHandler будут использовать одну и ту же блокировку, и поэтому в один момент только один поток может обновить рабочий элемент?

  3. Рабочий элемент имеет все общедоступные свойства получателя, кроме его свойства CalculatedValues. CalculatedValues ​​устанавливается только в статической блокировке. Этот код теперь потокобезопасен?

Ответы [ 2 ]

0 голосов
/ 04 июля 2018
  1. Нет, это не потокобезопасно. Если в каком-либо расчете есть какое-либо общее состояние, то могут возникнуть проблемы с многопоточностью. Единственный способ избежать проблем с многопоточностью - убедиться, что вы не обновляете общее состояние. Это означает, что объекты только для чтения и / или с использованием «чистых» функций .

  2. Вы использовали слово «shared» - это означает, что он не является потокобезопасным в силу состояния совместного использования. Если только вы не имеете в виду «распределенный», а не «общий».

  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, весь остальной код выглядит так, как будто все будет в порядке.

0 голосов
/ 04 июля 2018

1) Создание нового экземпляра класса, даже одного без открытых свойств, не дает никаких гарантий безопасности потоков. Проблема в том, что ExecuteAllCalculators принимает два параметра объекта. Объект WorkItem содержит изменяемые свойства, и один и тот же объект WorkItem используется для всех вызовов ICalculator. Предположим, один из калькуляторов решает вызвать Clear () для WorkItem.CalculatedValues. Или предположим, что один калькулятор устанавливает для WorkItem.Name значение null, а следующий решает сделать WorkItem.Name.Length. Технически это не проблема «многопоточности», потому что эти проблемы могут возникать без участия нескольких потоков.

2) Объекты калькулятора, разделяемые между потоками, определенно не безопасны для потоков. Предположим, что один из экземпляров калькулятора использует переменную уровня класса. Если эта переменная не защищена каким-либо потоком (пример: блокировка {...}), то можно будет получить противоречивые результаты. В зависимости от того, насколько «креативным» мог быть исполнитель экземпляров калькулятора, возможна тупиковая ситуация.

3) Каждый раз, когда ваш код принимает интерфейсы, вы приглашаете людей «поиграть в вашей песочнице». Это позволяет коду, над которым у вас мало контроля, выполняться. Один из лучших способов справиться с этим - использовать неизменяемые объекты. К сожалению, вы не можете изменить определение WorkItem, не нарушив контракт интерфейса.

4) Калькуляторы передаются по ссылке. Код показывает, что _myCalculators используется для всех созданных задач. Это не гарантирует, что у вас будут проблемы, это только делает возможным, что у вас могут быть проблемы.

...