Как мне узнать, является ли этот метод C # потокобезопасным? - PullRequest
48 голосов
/ 07 января 2009

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

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

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

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

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

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

Ответы [ 11 ]

52 голосов
/ 07 января 2009

Эта функция 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. 
38 голосов
/ 07 января 2009

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

 class BadCounter
 {
       private static int counter;

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

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

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

22 голосов
/ 07 января 2009

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

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

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

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

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

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

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

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

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

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

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

7 голосов
/ 07 января 2009

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

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

3 голосов
/ 07 января 2009

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

3 голосов
/ 07 января 2009

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

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

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

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

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

private int myVar = 0;

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

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

2 голосов
/ 07 января 2009

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

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

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

1 голос
/ 07 января 2009

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

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


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

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

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

0 голосов
/ 07 января 2009

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

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

0 голосов
/ 07 января 2009

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

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