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