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

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

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

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

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

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

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


person MatthewMartin    schedule 07.01.2009    source источник


Ответы (11)


Эта функция 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. 
person Cybis    schedule 07.01.2009
comment
Что делать, если x является типом значения, но передается по ссылке - static void MyFunction(ref int x) { ... }. Будет ли он по-прежнему ориентирован на многопоточность? - person RBT; 06.06.2017

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

 class BadCounter
 {
       private static int counter;

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

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

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

person Jon Skeet    schedule 07.01.2009
comment
Если ваше переданное значение или неизменяемый тип, такой как строка, вы должны быть в безопасности, чтобы изменить его правильно? Если вы передали эталонный объект, тогда он стал серым? - person JoshBerke; 07.01.2009
comment
@Josh: В общем, если вы передаете что-то методу, то изменение этого элемента является потокобезопасным, потому что каждый вызывающий объект передает свой собственный элемент. Ключевым моментом в примере Джона является то, что каждый поток будет изменять один и тот же статический счетчик членов. - person Dave Costa; 07.01.2009
comment
Если он неизменен, вы не сможете его изменить :) Однако вызов метода, который просто создает новый экземпляр (например, string.Replace), безопасен. (Между прочим, строка по-прежнему является ссылочным типом.) Серая область - это если вы передали ссылку на изменяемый объект - методы экземпляра на нем (продолжение) - person Jon Skeet; 07.01.2009
comment
может быть небезопасным, но вы можете рассматривать это как проблему, с которой вызывающий объект должен разобраться (т.е. два потока могут вызывать метод одновременно, но с разными аргументами). - person Jon Skeet; 07.01.2009
comment
Да, я перечитал свой комментарий, и я вижу, что мой разум мыслил быстрее, чем мои пальцы печатали, это в основном то, что я понял, что изменяемые ссылочные типы являются проблемой (поэтому строка безопасна). - person JoshBerke; 07.01.2009
comment
Ваше базовое определение, связанное с мутацией состояния, безусловно, дает душевное спокойствие, когда кто-то пытается понять безопасность потоков, но это, конечно, не заканчивается здесь, и все это внезапно превращается в водоворот осложнений. - person RBT; 06.06.2017

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

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

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

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

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

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

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

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

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

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

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

person Rob Parker    schedule 07.01.2009
comment
Я не понимаю ни одного слова, которое вы только что сказали, но +1 за усилия. - person Jacobs Data Solutions; 27.01.2010
comment
Вам следует завести блог, тогда вам не понадобятся вопросы, чтобы писать статьи! - person JJoos; 23.06.2010

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

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 больше нет смысла, но эй, это образец.

person JoshBerke    schedule 07.01.2009

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

person Thomas Danecker    schedule 07.01.2009

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

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

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

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

Тривиальный пример небезопасной версии вашей функции, не связанной с потоками, может быть такой:

private int myVar = 0;

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

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

person TheSmurf    schedule 07.01.2009
comment
Я не думаю, что это будет компилироваться, потому что статические члены не могут получить доступ к членам экземпляра. (addOne не может получить доступ к myVar.) - person Jeroen Landheer; 03.11.2012
comment
Вы правы, этого не произойдет. Слишком долго в прошлом, чтобы помнить, почему это так, поэтому просто исправил. - person TheSmurf; 23.01.2013

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

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

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

person Quibblesome    schedule 07.01.2009

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

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


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

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

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

person daniel    schedule 07.01.2009
comment
MethodImplOptions.Synchronized - не лучший выбор, потому что он блокирует это. Я бы предпочел заблокировать отдельный закрытый член, чтобы скрыть эти детали реализации от внешнего мира (разделение проблем, ...) - person Thomas Danecker; 09.01.2009

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

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

person Oskar    schedule 07.01.2009
comment
Ваше первое утверждение неверно. Два потока могут иметь ссылку на строку и могут одновременно вызывать методы строки без каких-либо негативных последствий. Даже изменяемый объект может быть поточно-ориентированным с осторожностью - даже без блокировки, если вы используете Interlocked и т. Д. Это непросто, но возможно. - person Jon Skeet; 07.01.2009

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

person yfeldblum    schedule 07.01.2009

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

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

person quamrana    schedule 07.01.2009