Многопоточный доступ к объектам

265
24 июня 2022, 07:40

Допустим имеется метод обращающийся к полям класса, метод вызывается в потоках. Как я понял, поля класса являются разделяемыми между всеми потоками с этим методом, и нужно обращаться к ним с помощью примитивов синхронизации(lock’ать). А объекты создаваемые в методе не являются общими между потоками и не могут пересечься, следовательно к ним общий доступ реализовывать не нужно. Но не совсем понятно, что происходит когда к объекту создаваемому присваивается значение поля, поэтому приходится и объект локать. Собственно вопрос: что именно нужно локать в методе, а что нет. И для чего разделяемые поля часто помечают модификатором static? И не понятно почему поток может застрять внутри лока функции pop, если здесь Monitor.Enter и Exit, в конструкции Wait не используется.

bool TryPop(out int num){
        lock(_locker){
            return _stack.TryPop(out num);
        }
    }
    void Push(int num){
        lock(_locker){
            _stack.Push(num);
    }
}

Спасибо за прочтение, подскажите куда копать.

Answer 1

Если говорить очень упрощенно (то есть это не всегда верно, но для понимания сойдет), то в C# есть 2 области памяти: стек и куча.

Куча

Куча служит для хранения объектов. Например, экземпляров классов. Эта куча доступна для любого потока вашей программы. Вопрос только в том, знает ли поток что из жтой кучи взять. Напирмер, если у вас в куче хранится объект и поток А имеет ссылку на него, то есть он знает, то по такуому то адресу в куче лежит экземпляр такого то класса - то он может этот экземпляр класса считать и вызвать его методы/свойства/поля и тд. Но если поток Б не имеет ссылки на экземпляр и ему неоткуда её взять, то он так и не узнает, что там в куче. Простой пример.

void MyFunction()
{
    var myObject = new MyClass();
}

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

Теперь давайте усложним задачу и добавим поле

private MyClass myField = new MyClass();
void MyFunction()
{
    var myObject = myField;
}

И мы видим, что объект сам, то есть экземпляр MyClass также будет лежать в куче, но его область видимости не ограничена функцией. Любой из потоков может получить доступ к объекту через поле класса. Раз любой поток может это сделать, то значит любой поток может работать с этим объектом, вызывать методы, иенять состояние и тд. Код выше не является потокобезопасным.

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

Стек

Чтобы говорить о многопоточности и стеке, надо понимать, что такое стек. Стек - это, грубо говоря, временное хранилище состояний функций. Например, поглядит следующий код:

void MyFunction1()
{
    int i1 = 10;
    MyFunction2();
    i1 = 15;
}
void MyFunction2()
{
    int i2 = 10;
}

Допустим, мы вызываем функцию MyFunction1(), она начинает выполняться, и потом доходит до вызова MyFunction2(); - что при этом происходит? Как происходит вызов одной функции из другой?

Вот примерный псевлоалгоритм:

  • вызов функции MyFunction1()
  • поместить в стек int i1
  • записать в i1 значение 10
    • вызов функции MyFunction2();
    • поместить в стек int i2
    • записать в i2 значение 10
    • удалить из стека переменную i2
    • конец функции MyFunction2();
  • записать в i1 значение 15
  • удалить из стека переменную i1
  • конец функции MyFunction1();

Отсюда можно сделать несколько выводов:

  1. Стек содержит в себе состояние переменных функции.
  2. Стек растет при увеличении глубины вызовов методов.
  3. Стек - он отдельный для каждого потока. У каждого потока свой стек.

И вот тут в игру вступают отличия значимых и ссылочных типов. Поглядим на этот пример

private MyClass myField = new MyClass();
void MyFunction()
{
    var myObject = myField;
}

Здесь переменная myObject представляет собой адрес в памяти в куче. Это не сам объект - это инструкция, где его искать. Значение это переменной (то есть адрес объекта) будет храниться в стеке, но сам объект - в куче. Таким образом, другие потоки не смогут изменить значение переменной myObject, но они могут изменить объект, который ледит по адресу, куда указывает myObject.

А вот в этом примере

private int myField = 10;
void MyFunction()
{
    var myInt = myField;
}

Вот тут var myInt = myField; происходит копирование значения myField в переменую myInt и значение переменной myInt будет лежать в стеке. То есть после присваивания уже никто, кроме текущего потока, не сможет изменить значение myInt, так как оно полностью лежит в стеке и не имеет ничего общего с кучей.

Вопросы

И для чего разделяемые поля часто помечают модификатором static?

Это утверждение не соотвенствует действительности.

почему поток может застрять внутри лока функции pop, если здесь Monitor.Enter и Exit, в конструкции Wait не используется

lock - это и есть Monitor.Enter + Monitor.Exit + try-finally.

Answer 2

что именно нужно локать в методе, а что нет

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

Под разделяемым ресурсом тут понимается объект, к которому есть доступ из разных потоков.

Отсюда следует, что если к объекту возможен доступ только из одного потока, то нет смысла использовать блокировки при работе с ним. Это вы скорее всего имеете ввиду (но используете не совсем точные формулировки) тут:

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

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

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

для чего разделяемые поля часто помечают модификатором static

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

READ ALSO
Ошибка cs0120 как исправить

Ошибка cs0120 как исправить

Я не понимаю, как тут исправить данную ошибку:

265
Как заставить выполняться длительный код по нажатию кнопки ASP.NET Core

Как заставить выполняться длительный код по нажатию кнопки ASP.NET Core

У меня есть код который должен работать 24/7 и есть сервер через который я должен его запускатьТо есть отправил запрос http://site

253
Двери работают странно

Двери работают странно

Делаю 2д платформер, захотел двериПо идее должно работать так - есть дверь, и у нее есть linkeddoor, при нажатии E пока в двери происходит телепортация...

323