Что делает метод потокобезопасным? Каковы правила?

156

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

  1. Если метод обращается только к локальным переменным, это потокобезопасно.

Это оно? Это относится и к статическим методам?

Один ответ, предоставленный @Cybis, был:

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

Это относится и к статическим методам?

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

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

РЕДАКТИРОВАТЬ
Много хороших моментов было сделано здесь. Я думаю, что реальный ответ на этот вопрос: «Нет простых правил для обеспечения безопасности потоков». Прохладно. Хорошо. Но в целом я думаю, что принятый ответ дает хорошее, краткое резюме. Всегда есть исключения. Пусть будет так. Я могу жить с этим.

Боб Хорн
источник
59
Ты не должен обращаться к переменным, которые также доступны другим потокам без медальона.
Ганс Пассант
4
Хант Патант превратился в игора!
Мартин Джеймс
3
Кроме того ... «Вы не должны обращаться к переменным, которые также доступны другим потокам без медальона» - и это не имеет значения, если считываемое значение иногда не является последним или действительно неверно.
Мартин Джеймс
Вот хороший блог Эрика, который поможет вам погрузиться в бурю.
RBT

Ответы:

140

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

В этом случае несколько потоков могут вызывать ThreadSafeMethodодновременно без проблем.

public class Thing
{
    public int ThreadSafeMethod(string parameter1)
    {
        int number; // each thread will have its own variable for number.
        number = parameter1.Length;
        return number;
    }
}

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

public class Thing
{
    public int ThreadSafeMethod(string parameter1)
    {
        int number;
        number = this.GetLength(parameter1);
        return number;
    }

    private int GetLength(string value)
    {
        int length = value.Length;
        return length;
    }
}

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

public class Thing
{
    private string someValue; // all threads will read and write to this same field value

    public int NonThreadSafeMethod(string parameter1)
    {
        this.someValue = parameter1;

        int number;

        // Since access to someValue is not synchronised by the class, a separate thread
        // could have changed its value between this thread setting its value at the start 
        // of the method and this line reading its value.
        number = this.someValue.Length;
        return number;
    }
}

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

Для обеспечения правильного параллелизма вам нужно использовать блокировку.

Для получения дополнительной информации см. ссылку на оператор блокировки C # и ReadWriterLockSlim .

Блокировка в основном полезна для предоставления по одному функциональности за раз,
ReadWriterLockSlimполезна, если вам нужно несколько читателей и один писатель.

Тревор Пилли
источник
15
В третьем примере private string someValue;это не staticтак, каждый экземпляр получит отдельную копию этой переменной. Так что вы можете объяснить, как это не потокобезопасно?
Бхарадвай
29
@Bharadwaj, если есть один экземпляр Thingкласса, доступ к которому осуществляется несколькими потоками
Тревор Пилли
112

Если метод обращается только к локальным переменным, это потокобезопасно. Это оно?

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

https://stackoverflow.com/a/8883117/88656

Это относится и к статическим методам?

Точно нет.

Один из ответов, предоставленных @Cybis, звучал так: «Локальные переменные не могут совместно использоваться потоками, поскольку каждый поток получает свой собственный стек».

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

Это относится и к статическим методам?

Точно нет.

Если методу передается ссылочный объект, нарушает ли это безопасность потока?

Может быть.

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

Вам придется научиться жить с разочарованием. Это очень сложный предмет.

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

Нет. Как вы видели из моего примера ранее , пустой метод может быть не поточно-ориентированным . Вы также можете спросить: «Есть ли короткий список правил, обеспечивающих корректность метода ». Нет, нет Безопасность потоков - не что иное, как чрезвычайно сложный вид правильности.

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

Опять же, посмотрите на мой пример: каждый метод тривиален . Именно способ, которым методы взаимодействуют друг с другом на «глобальном» уровне, делает программу тупиковой. Вы не можете смотреть на каждый метод и отмечать его как «безопасный», а затем ожидать, что вся программа безопасна, больше, чем вы можете сделать вывод, что, поскольку ваш дом сделан из 100% -го пустотелого кирпича, этот дом также не полый. Пустота дома - это глобальная собственность всего этого, а не совокупность свойств его частей.

Эрик Липперт
источник
15
Базовое утверждение: безопасность потоков - это глобальное, а не локальное свойство программы.
Грег Д.
5
@BobHorn: вызовите class C { public static Func<int> getter; public static Action<int> setter; public static void M() { int x = 0; getter = ()=>x; setter = y=>{x=y;};} } M (), затем вызовите C.getter и C.setter в двух разных потоках. Теперь локальная переменная может записываться и считываться в двух разных потоках, даже если она локальная. Опять же: определяющей характеристикой локальной переменной является то, что она локальная , а не то, что она находится в стеке потока .
Эрик Липперт
3
@BobHorn: Я думаю, что это больше связано с пониманием наших инструментов на таком уровне, что мы можем говорить о них с авторитетом и знаниями. Например, исходный вопрос выдал отсутствие понимания того, что такое локальная переменная. Эта ошибка встречается довольно часто, но дело в том, что это ошибка, которую следует исправить. Определение локальной переменной является довольно точным и должно рассматриваться соответствующим образом. :) Это не ответ для "людей", которые не понимают, что патологические случаи существуют. Это разъяснение, чтобы вы могли понять, что вы на самом деле спросили. :)
Грег Д.
7
@EricLippert: документация MSDN для многих классов объявляет, что 'Public static (Shared в Visual Basic) члены этого типа являются поточно-ориентированными. Любые члены экземпляра не гарантируют многопоточность. ' или иногда «Этот тип является потокобезопасным.» (например, String), как эти гарантии можно сделать, когда безопасность потоков является глобальной проблемой?
Майк Зборай
3
@mikez: Хороший вопрос. Либо эти методы не изменяют никакое состояние, либо, когда они это делают, они изменяют состояние безопасно без блокировок, или, если они используют блокировки, они делают это таким образом, чтобы гарантировать, что глобальный «порядок блокировки» не будет нарушен. Строки - это неизменяемые структуры данных, методы которых ничего не изменяют, поэтому можно легко сделать строку безопасной.
Эрик Липперт
12

Там нет жесткого и быстрого правила.

Вот некоторые правила, чтобы сделать потокобезопасным код в .NET, и почему это не очень хорошие правила:

  1. Функция и все функции, которые она вызывает, должны быть чистыми (без побочных эффектов) и использовать локальные переменные. Хотя это сделает ваш код поточно-ориентированным, очень мало интересных вещей, которые вы можете сделать с этим ограничением в .NET.
  2. Каждая функция, которая работает с общим объектом, должна lockиметь общую вещь. Все замки должны быть выполнены в одинаковом порядке. Это сделает поток кода безопасным, но это будет невероятно медленно, и вы можете также не использовать несколько потоков.
  3. ...

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

earlNameless
источник
10
Замки не должны быть медленными. Замки невероятно быстрые; бесспорная блокировка составляет порядка десяти-ста наносекунд . Оспариваемые блокировки, конечно, произвольно медленные; если вы замедлились из-за того, что боретесь с блокировками, то заново отредактируйте программу, чтобы устранить конфликт .
Эрик Липперт
2
Я полагаю, что не смог сделать № 2 достаточно ясным. Я пытался сказать, что есть способ сделать потоки безопасными: ставить блокировки вокруг каждого доступа к любым обычным объектам. И если предположить, что есть несколько потоков, это вызовет конкуренцию, и блокировки замедлят процесс. При добавлении замков нельзя просто слепо добавлять замки, их нужно размещать в очень хороших стратегических местах.
EarlNameless