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

Как узнать, является ли этот метод С# потоком безопасным?

Я работаю над созданием функции обратного вызова для события удаления элемента кэша ASP.NET.

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

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

Часть 2: Означает ли это, что если у меня есть

static int addOne(int someNumber){
    int foo = someNumber;
    return foo +1; 
}

и я вызываю Class.addOne(5); и Class.addOne(6); Одновременно, могу ли я получить 6 или 7 возвращенных в зависимости от того, кто в первый раз называет foo? (т.е. состояние гонки)

4b9b3361

Ответ 1

Эта функция addOne действительно является потокобезопасной, поскольку она не имеет доступа к каким-либо данным, к которым может обращаться другой поток. Локальные переменные не могут быть разделены между потоками, потому что каждый поток получает свой собственный стек. Однако вам нужно убедиться, что параметры функции являются типами значений, а не ссылочными типами.

static void MyFunction(int x) { ... } // thread safe. The int is copied onto the local stack.

static void MyFunction(Object o) { ... } // Not thread safe. Since o is a reference type, it might be shared among multiple threads. 

Ответ 2

Нет, addOne здесь потокобезопасен - он использует только локальные переменные. Вот пример, который не был бы потокобезопасным:

 class BadCounter
 {
       private static int counter;

       public static int Increment()
       {
             int temp = counter;
             temp++;
             counter = temp;
             return counter;
       }
 }

Здесь два потока могут одновременно вызвать "Приращение" и в конечном итоге увеличивать только один раз. (Использование return ++counter; было бы так же плохо, кстати, - это более явная версия того же самого. Я расширил его, чтобы было более очевидно неправильно.)

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

Ответ 3

Проблемы с потоками (о которых я также беспокоился в последнее время) возникают из-за использования нескольких процессорных ядер с отдельными кэшами, а также из основных условий гонки с переключением потоков. Если кэши для отдельных ядер попадают в одно и то же место памяти, они, как правило, не имеют представления о другом, и могут отдельно отслеживать состояние этого местоположения без его возврата в основную память (или даже в синхронизированный кэш, общий для всех ядра на L2 или L3, например), по причинам производительности процессора. Таким образом, даже приемы блокировки порядка выполнения могут быть ненадежными в многопоточных средах.

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

Вы можете получить блокировку экземпляра любого ссылочного типа (например, наследует от Object, а не типы значений, такие как int или перечисления, а не null), но очень важно понять, что блокировка объекта не присуща влияет на доступ к этому объекту, он только взаимодействует с другими попытками получить блокировку на одном и том же объекте. Это зависит от класса для защиты доступа к его переменным-членам с использованием соответствующей схемы блокировки. Иногда экземпляры могут защищать многопоточные обращения к своим собственным элементам, блокируя себя (например, lock (this) { ... }), но обычно это необязательно, поскольку экземпляры, как правило, удерживаются только одним владельцем и не должны гарантировать поточный доступ к экземпляр.

Чаще всего класс создает частный замок (например. private readonly object m_Lock = new Object(); для отдельных блокировок в каждом экземпляре для защиты доступа к членам этого экземпляра или private static readonly object s_Lock = new Object(); для центрального замка для защиты доступа к статическим членам класса), У Джоша есть более конкретный пример кода использования блокировки. Затем вам нужно закодировать класс для надлежащего использования блокировки. В более сложных случаях вам может потребоваться создать отдельные блокировки для разных групп участников, чтобы уменьшить конкуренцию для разных ресурсов, которые не используются вместе.

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

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

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

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

При работе с классами .NET Framework документы Microsoft в MSDN, независимо от того, является ли данный вызов API потокобезопасным (например, статические методы предоставленных общих типов коллекций, таких как List<T>, становятся потокобезопасными, в то время как методы экземпляра могут не быть - но обязательно проверяйте). Подавляющее большинство времени (и, если только в нем конкретно не указано, что это поточно-безопасное), оно не является внутренним поточно-безопасным, поэтому вы несете ответственность за его безопасное использование. И даже если отдельные операции реализованы внутри поточно-безопасными, вам все равно придется беспокоиться о совместном и перекрывающемся доступе к вашему коду, если он делает что-то более сложное, что должно быть атомарным.

Одно большое предостережение - итерирование по коллекции (например, с помощью foreach). Даже если каждый доступ к коллекции получает стабильное состояние, нет никакой гарантии, что он не будет изменяться между этими обращениями (если где-нибудь еще можно добраться до него). Когда коллекция хранится локально, обычно нет проблем, но коллекция, которая может быть изменена (другим потоком или во время выполнения цикла!), Может привести к непоследовательным результатам. Один простой способ решить эту проблему - использовать атомную поточно-безопасную операцию (внутри вашей защитной схемы блокировки), чтобы сделать временную копию коллекции (MyType[] mySnapshot = myCollection.ToArray();), а затем повторить эту локальную копию моментального снимка вне блокировки. Во многих случаях это позволяет избежать необходимости держать блокировку все время, но в зависимости от того, что вы делаете в ходе итерации, этого может быть недостаточно, и вам просто нужно защищать от изменений все время (или вы, возможно, уже имеете его внутри заблокированная секция, защищающая доступ к изменению коллекции вместе с другими вещами, поэтому она покрыта).

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

Ответ 4

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

static int foo;

static int addOne(int someNumber)
{
  foo=someNumber; 
  return foo++;
}

Это не безопасный поток, потому что мы касаемся статических данных. Затем это необходимо изменить следующим образом:

static int foo;
static object addOneLocker=new object();
static int addOne(int someNumber)
{
  int myCalc;
  lock(addOneLocker)
  {
     foo=someNumber; 
     myCalc= foo++;
  }
  return myCalc;
}

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

Ответ 5

Это будет только условие гонки, если оно изменяет некоторую переменную, внешнюю по отношению к функции. Ваш пример не делает этого.

Это в основном то, что вы ищете. Защита резьбы означает, что функция:

  • Не изменяет внешние данные или
  • Доступ к внешним данным должным образом синхронизирован, так что только одна функция может получить к нему доступ в любой момент времени.

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

Тривиальный пример безошибочной версии вашей функции будет следующим:

private int myVar = 0;

private void addOne(int someNumber)
{
   myVar += someNumber;
}

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

Ответ 6

Происходит какое-то исследование, позволяющее обнаруживать небезопасный код. Например. проект CHESS в Microsoft Research.

Ответ 7

В приведенном выше примере нет.

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

static int myInt;

static int addOne(int someNumber){
myInt = someNumber;
return myInt +1; 
}

Это будет означать, что из-за переключения контекста 1 может перейти к вызову myInt = someNumber, а затем контекстному переключателю, скажем, что поток 1 просто устанавливает его на 5. Тогда представьте, что поток 2 входит и использует 6 и возвращает 7. Затем, когда поток 1 снова просыпается, он будет иметь 6 в myInt вместо 5, которые он использовал, и возвращает 7 вместо ожидаемого 6.: O

Ответ 8

В любом месте потокобезопасный означает, что при обращении к ресурсу у вас нет двух или более потоков. Обычно статические varaiables --- в таких языках, как С#, VB.NET и Java, сделали ваш код небезопасным.

В Java существует синхронизированное ключевое слово. Но в .NET вы получаете опцию/директиву сборки:


class Foo
{
    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Bar(object obj)
    {
        // do something...
    }
}

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

Если вам не нужен синхронизированный метод, вы можете попробовать метод блокировки, например, spin-lock.

Ответ 9

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

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

Ответ 10

foo не разделяется между параллельными или последовательными вызовами, поэтому addOne является потокобезопасным.

Ответ 11

Причина, по которой "foo" и "someNumber" безопасна в вашем примере, заключается в том, что они находятся в стеке, и каждый поток имеет свой собственный стек и поэтому не является общим.

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