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

Безопасность потока С# с get/set

Это подробный вопрос для С#.

Предположим, что у меня есть класс с объектом, и этот объект защищен блокировкой:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         return property;
    }
    set { 
         property = value; 
    }
}

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

Будет ли следующий код правильно блокировать данные?

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         lock (mLock){
             return property;
         }
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

"Правильно", я имею в виду, если я хочу позвонить

MyProperty.Field1 = 2;

или что-то еще, будет ли поле заблокировано во время обновления? Является ли настройка, выполняемая оператором equals внутри области функции get, или функция "get" (и, следовательно, блокировка) заканчивается сначала, а затем вызывается настройка, а затем "set", минуя замок?

Изменить: так как это, видимо, не будет делать трюк, что будет? Мне нужно сделать что-то вроде:

Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
    get {
         MyObject tmp = null;
         lock (mLock){
             tmp = property.Clone();
         }
         return tmp;
    }
    set { 
         lock (mLock){
              property = value; 
         }
    }
}

который более или менее просто гарантирует, что у меня есть только доступ к копии, что означает, что если бы у меня было два потока, называющих "get" в одно и то же время, каждый из них начнет с того же значения Field1 (справа?). Есть ли способ сделать чтение и запись блокировки на свойстве, которое имеет смысл? Или я должен просто ограничить себя блокировкой на разделах функций, а не самих данных?

Просто, чтобы этот пример имел смысл: MyObject - это драйвер устройства, который возвращает статус асинхронно. Я отправляю команды через последовательный порт, а затем устройство реагирует на эти команды в свое время. Прямо сейчас у меня есть поток, который опросает его для своего статуса ( "Вы все еще там? Можете ли вы принять команды?" ), Поток, ожидающий ответов на последовательный порт ( "Только что получил строку состояния 2, все хорошее" )), а затем поток пользовательского интерфейса, который принимает другие команды ( "Пользователь хочет, чтобы вы делали это".) и отправляет ответы от драйвера ( "Я только что сделал, теперь обновляю интерфейс с этим" ). Поэтому я хочу блокировать сам объект, а не поля объекта; это было бы огромное количество блокировок, а и b, не все устройства этого класса имеют одинаковое поведение, просто общее поведение, поэтому мне пришлось бы кодировать множество отдельных диалогов, если бы я индивидуализировал блокировки.

4b9b3361

Ответ 1

Нет, ваш код не будет блокировать доступ к членам объекта, возвращаемого с MyProperty. Он блокирует только MyProperty.

В вашем примере используется две операции, свернутые в один, примерно эквивалентный этому:

// object is locked and then immediately released in the MyProperty getter
MyObject o = MyProperty;

// this assignment isn't covered by a lock
o.Field1 = 2;

// the MyProperty setter is never even called in this example

В двух словах - если два потока обращаются к MyProperty одновременно, то получатель коротко блокирует второй поток, пока он не вернет объект в первый поток, но затем вернет объект ко второму потоку. Оба потока будут иметь полный, разблокированный доступ к объекту.

EDIT в ответ на дальнейшие подробности в вопросе

Я все еще не уверен на 100%, чего вы пытаетесь достичь, но если вы просто хотите получить атомный доступ к объекту, тогда у вас не будет блокировки вызывающего кода против самого объекта?

// quick and dirty example
// there almost certainly a better/cleaner way to do this
lock (MyProperty)
{
    // other threads can't lock the object while you're in here
    MyProperty.Field1 = 2;
    // do more stuff if you like, the object is all yours
}
// now the object is up-for-grabs again

Не идеально, но пока весь доступ к объекту содержится в разделах lock (MyProperty), тогда этот подход будет потокобезопасным.

Ответ 2

Параллельное программирование будет довольно простым, если ваш подход может работать. Но это не так, айсберг, который тонет, что Титаник, например, клиент вашего класса делает это:

objectRef.MyProperty += 1;

Гонка чтения-модификации-записи довольно очевидна, есть и худшие. Вы абсолютно ничего не можете сделать, чтобы сделать вашу недвижимость потокобезопасной, за исключением того, чтобы сделать ее неизменной. Это ваш клиент, который должен справиться с головной болью. Будучи вынужденным делегировать такую ​​ответственность программисту, который наименее правдоподобен, это ахиллесова пята параллельного программирования.

Ответ 3

Как указывали другие, как только вы возвращаете объект из геттера, вы теряете контроль над тем, кто обращается к объекту и когда. Чтобы сделать то, что вы хотите сделать, вам нужно поместить блокировку внутри самого объекта.

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

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

В качестве примера...

public class Status
{
    private int _code;
    private DateTime _lastUpdate;
    private object _sync = new object(); // single lock for both fields

    public int Code
    {
        get { lock (_sync) { return _code; } }
        set
        {
            lock (_sync) {
                _code = value;
            }

            // Notify listeners
            EventHandler handler = Changed;
            if (handler != null) {
                handler(this, null);
            }
        }
    }

    public DateTime LastUpdate
    {
        get { lock (_sync) { return _lastUpdate; } }
        set { lock (_sync) { _lastUpdate = value; } }
    }

    public event EventHandler Changed;
}

Ваш поток опроса будет выглядеть примерно так.

Status status = new Status();
ManualResetEvent changedEvent = new ManualResetEvent(false);
Thread thread = new Thread(
    delegate() {
        status.Changed += delegate { changedEvent.Set(); };
        while (true) {
            changedEvent.WaitOne(Timeout.Infinite);
            int code = status.Code;
            DateTime lastUpdate = status.LastUpdate;
            changedEvent.Reset();
        }
    }
);
thread.Start();

Ответ 4

Область блокировки в вашем примере находится в неправильном месте - она ​​должна находиться в области свойства класса MyObject, а не контейнера.

Если MyObject мой класс объектов просто используется для хранения данных, которые один поток хочет записать, а другой (поток пользовательского интерфейса) для чтения, то вам может не понадобиться сеттер вообще и построить его один раз.

Также рассмотрим, является ли размещение блокировок на уровне свойства уровнем записи блокировки; если для представления состояния транзакции (например, общих заказов и общего веса) может быть записано более одного свойства, то лучше было бы блокировать уровень MyObject (например, lock (myObject.SyncRoot))..)

Ответ 5

В приведенном вами примере кода получение никогда не выполняется.

В более сложном примере:

MyProperty.Field1 = MyProperty.doSomething() + 2;

И, конечно, предполагая, что вы сделали:

lock (mLock) 
{
    // stuff...
}

В doSomething() тогда все вызовы блокировки не будут достаточными, чтобы гарантировать синхронизацию по всему объекту. Как только функция doSomething() вернется, блокировка будет потеряна, тогда добавление будет завершено, а затем произойдет назначение, которое снова закроется.

Или, чтобы написать это другим способом, вы можете притворяться, что блокировки не выполняются amutomatically и переписывают это больше как "машинный код" с одной операцией в строке, и становится очевидным:

lock (mLock) 
{
    val = doSomething()
}
val = val + 2
lock (mLock)
{
    MyProperty.Field1 = val
}

Ответ 6

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

Код, который вы опубликовали, заблокировал элемент, пока он читается и записывается. Если вы хотите обрабатывать случай, когда значение обновляется, возможно, вам следует изучить другие формы синхронизации, такие как события. (Проверьте версии авто/руководства). Затем вы можете сообщить своему потоку "опроса", что значение изменилось и оно будет перечитано.

Ответ 7

В вашей отредактированной версии вы по-прежнему не предоставляете потоковый режим обновления MyObject. Любые изменения свойств объекта должны выполняться внутри блока синхронизации/блокировки.

Вы можете написать отдельные сеттеры, чтобы справиться с этим, но вы указали, что это будет сложно из-за полей большого числа. Если это действительно так (и вы еще не дали достаточной информации, чтобы оценить это), один из вариантов заключается в том, чтобы написать сеттер, который использует отражение; это позволит вам передать строку, представляющую имя поля, и вы можете динамически искать имя поля и обновлять значение. Это позволит вам иметь один сеттер, который будет работать на любом количестве полей. Это не так просто или эффективно, но это позволит вам иметь дело с большим количеством классов и полей.

Ответ 8

Вы внедрили блокировку для получения/установки объекта, но вы не сделали поток объектов безопасным, что является другой историей.

Я написал статью о неизменяемых классах моделей на С#, которые могут быть интересны в этом контексте: http://rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel-world/

Ответ 9

Блокировки С# не страдают от тех же проблем с блокировкой, что и другие языки?

например.

var someObj = -1;

// Thread 1

if (someObj = -1)
    lock(someObj)
        someObj = 42;

// Thread 2

if (someObj = -1)
    lock(someObj)
        someObj = 24;

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

// Threads 1 & 2

if (someObj = -1)
    lock(someObj)
        if(someObj = -1)
            someObj = {newValue};

Просто что-то иметь в виду.