Подтвердить что ты не робот

Является ли это хорошим дизайном для создания безопасных классов в С#?

Часто, когда я хочу класс, который является потокобезопасным, я делаю что-то вроде следующего:

public class ThreadSafeClass
{
    private readonly object theLock = new object();

    private double propertyA;
    public double PropertyA
    {
        get
        {
            lock (theLock)
            {
                return propertyA;
            }
        }
        set
        {
            lock (theLock)
            {
                propertyA = value;
            }
        }
    }

    private double propertyB;
    public double PropertyB
    {
        get
        {
            lock (theLock)
            {
                return propertyB;
            }
        }
        set
        {
            lock (theLock)
            {
                propertyB = value;
            }
        }
    }

    public void SomeMethod()
    {
        lock (theLock)
        {
            PropertyA = 2.0 * PropertyB;
        }
    }
}

Это работает, но это очень много. Иногда я даже создаю объект блокировки для каждого метода и свойства, создавая большую детализацию и сложность.

Я знаю, что также можно блокировать классы, используя атрибут Synchronization, но я не уверен, насколько хорошо эти масштабы - поскольку я часто ожидаю иметь сотни тысяч, если не миллионы, экземпляров потокобезопасных объектов, Этот подход создавал бы контекст синхронизации для каждого экземпляра класса и требовал, чтобы класс был выведен из ContextBoundObject и поэтому не мог быть получен из чего-либо другого - поскольку С# не допускает множественного наследования - это шоу-стоппер во многих случаях.

Изменить: как подчеркнули некоторые из респондентов, нет никакой концепции создания "потоковой безопасности" с "серебряной пулей". Я просто пытаюсь понять, является ли шаблон, который я использую, одним из хороших решений. Конечно, лучшее решение в любой конкретной ситуации зависит от проблемы. Некоторые из приведенных ниже ответов содержат альтернативные варианты, которые следует учитывать.

Изменить: Кроме того, существует более одного определения безопасности потоков. Например, в моей реализации выше, следующий код НЕ был бы потокобезопасным:

var myObject = new ThreadSafeClass();
myObject.PropertyA++; // NOT thread-safe

Итак, действительно ли определение класса представляет собой хороший подход? Если нет, что бы вы порекомендовали для дизайна с аналогичным поведением, которое было бы потокобезопасным для аналогичного набора применений?

4b9b3361

Ответ 1

В решении многопоточности нет решения "одноразового доступа". Проведите некоторое исследование по созданию неизменяемых классов и узнайте о разных примитивах синхронизации.

Это пример полунепрерывного или не-программируемого класса.

public class ThreadSafeClass
{
    public double A { get; private set; }
    public double B { get; private set; }
    public double C { get; private set; }

    public ThreadSafeClass(double a, double b, double c)
    {
        A = a;
        B = b;
        C = c;
    }

    public ThreadSafeClass RecalculateA()
    {
        return new ThreadSafeClass(2.0 * B, B, C);
    }
}

В этом примере выполняется перенос кода синхронизации в другой класс и сериализация доступа к экземпляру. На самом деле, вы действительно не хотите больше, чем один поток, работающий на объекте в любой момент времени.

public class ThreadSafeClass
{
    public double PropertyA { get; set; }
    public double PropertyB { get; set; }
    public double PropertyC { get; set; }

    private ThreadSafeClass()
    {

    }

    public void ModifyClass()
    {
        // do stuff
    }

    public class Synchronizer
    {
        private ThreadSafeClass instance = new ThreadSafeClass();
        private readonly object locker = new object();

        public void Execute(Action<ThreadSafeClass> action)
        {
            lock (locker)
            {
                action(instance);
            }
        }

        public T Execute<T>(Func<ThreadSafeClass, T> func)
        {
            lock (locker)
            {
                return func(instance);
            }
        }
    }
}

Вот краткий пример того, как вы его используете. Это может показаться немного неуклюжим, но это позволяет выполнять много действий на экземпляре за один раз.

var syn = new ThreadSafeClass.Synchronizer();

syn.Execute(inst => { 
    inst.PropertyA = 2.0;
    inst.PropertyB = 2.0;
    inst.PropertyC = 2.0;
});

var a = syn.Execute<double>(inst => {
    return inst.PropertyA + inst.PropertyB;
});

Ответ 2

Я знаю, что это может звучать как умный ответ **, но... ЛУЧШИЙ способ разработки потокобезопасных классов - это действительно знать о многопоточности, о ее последствиях, своих тонкостях и о том, что она подразумевает. Там нет серебряной пули.

Серьезно... не пытайтесь многопоточно (в сценариях производства я имею в виду), пока вы не узнаете, в чем вы попадаете... Это может быть огромная ошибка.

Изменить: Вы должны, конечно, знать примитивы синхронизации как операционной системы, так и вашего языка (С# под Windows в этом случае, я думаю).

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

Ответ 3

Помните, что термин "безопасный поток" не является конкретным; то, что вы здесь делаете, будет более точно обозначаться как "синхронизация" с помощью блокировки Monitor.

Тем не менее, многословие вокруг синхронизированного кода в значительной степени неизбежно. Вы можете сократить некоторые пробелы в своем примере, изменив такие вещи:

lock (theLock)
{
    propertyB = value;
}

в это:

lock (theLock) propertyB = value;

Что касается того, действительно ли это подходит вам, нам действительно нужна дополнительная информация. Синхронизация - это всего лишь один подход к "безопасности потоков"; неизменяемые объекты, семафоры и т.д. - все это разные механизмы, которые подходят для разных прецедентов. Для простого примера, который вы предоставляете (где, похоже, вы пытаетесь обеспечить атомарность операции get или set), тогда похоже, что вы сделали правильные вещи, но если ваш код предназначен для большей иллюстрации, чем пример, тогда все может быть не так просто.

Ответ 4

Так как больше ничего подобного не делает, вот какой-то анализ вашего конкретного дизайна.

  • Хотите прочитать какое-либо одно свойство? THREADSAFE
  • Хотите обновить какое-либо одно свойство? THREADSAFE
  • Хотите прочитать одно свойство и затем обновить его на основе его первоначального значения? Не Threadsafe

Thread 2 может обновить значение между чтением и обновлением потока 1.

  • Хотите обновить два связанных свойства одновременно? Не Threadsafe

В итоге вы можете получить свойство A, имеющее значение потока 1, и свойство B, имеющее значение потока 2.

  • Тема 1 Обновление A
  • Тема 2 Обновление A
  • Тема 1 Обновление B
  • Тема 2 Обновление B

    • Хотите одновременно прочитать два связанных свойства? Не Threadsafe

Снова, вы можете быть прерваны между первым и вторым чтением.

Я мог бы продолжить, но вы поняли эту идею. Threadsafety основан исключительно на том, как вы планируете доступ к объектам и что вам нужно сделать promises.

Ответ 5

Вы можете найти класс Interlocked. Он содержит несколько атомных операций.

Ответ 6

Одна вещь, которую вы могли бы сделать, которая могла бы помочь вам избежать дополнительного кода, - это использовать что-то вроде PostSharp для автоматического добавления этих операторов lock в ваш код, даже если у вас их сотни. Все, что вам нужно - это один атрибут, присоединенный к классу, и реализация атрибута, которая добавит дополнительные блокирующие переменные.

Ответ 7

В соответствии с моим комментарием выше - он становится немного причудливым, если вы хотите, чтобы одновременно разрешались читатели, но разрешен только один писатель. Обратите внимание: если у вас есть .NET 3.5, используйте ReaderWriterLockSlim вместо ReaderWriterLock для этого типа шаблона.

public class ThreadSafeClass
{
    private readonly ReaderWriterLock theLock = new ReaderWriterLock();

    private double propertyA;
    public double PropertyA
    {
        get
        {
            theLock.AcquireReaderLock(Timeout.Infinite);
            try
            {
                return propertyA;
            }
            finally
            {
                theLock.ReleaseReaderLock();
            }
        }
        set
        {
            theLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                propertyA = value;
            }
            finally
            {
                theLock.ReleaseWriterLock();
            }
        }
    }

    private double propertyB;
    public double PropertyB
    {
        get
        {
            theLock.AcquireReaderLock(Timeout.Infinite);
            try
            {
                return propertyB;
            }
            finally
            {
                theLock.ReleaseReaderLock();
            }
        }
        set
        {
            theLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                propertyB = value;
            }
            finally
            {
                theLock.ReleaseWriterLock();
            }
        }
    }

    public void SomeMethod()
    {
        theLock.AcquireWriterLock(Timeout.Infinite);
        try
        {
            theLock.AcquireReaderLock(Timeout.Infinite);
            try
            {
                PropertyA = 2.0 * PropertyB;
            }
            finally
            {
                theLock.ReleaseReaderLock();
            }
        }
        finally
        {
            theLock.ReleaseWriterLock();
        }
    }
}