Проблемы с многопоточностью (о которых я также беспокоился в последнее время) возникают из-за использования нескольких процессорных ядер с отдельными кэш-памятью, а также из-за основных условий гонки с перестановкой потоков. Если кеши для отдельных ядер обращаются к одной и той же ячейке памяти, они, как правило, не имеют представления о другой ячейке и могут отдельно отслеживать состояние этой ячейки данных, не возвращая ее в основную память (или даже в синхронизированный кеш, общий для всех ядра на 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();
), а затем итерация по этой локальной копии снимка за пределами блокировки. Во многих случаях это избавляет от необходимости удерживать блокировку все время, но в зависимости от того, что вы делаете в итерации, этого может быть недостаточно, и вам просто нужно постоянно защищаться от изменений (или вы уже можете иметь его внутри заблокированная секция, защищающая от доступа, чтобы изменить коллекцию наряду с другими вещами, так что она покрыта).
Итак, в поточно-ориентированном дизайне есть немного искусства, и знание того, где и как получить блокировки для защиты вещей, во многом зависит от общего дизайна и использования вашего класса (классов). Может быть легко стать параноиком и подумать, что вам нужно запирать повсюду все, но на самом деле речь идет о поиске подходящего слоя для защиты вещей.