线程问题(我最近也一直在担心)来自使用具有单独缓存的多个处理器内核,以及基本的线程交换竞争条件。如果不同内核的缓存访问相同的内存位置,它们通常不会知道另一个,并且可能会单独跟踪该数据位置的状态,而不会返回到主内存(甚至是在所有内存中共享的同步缓存)例如 L2 或 L3 的核心),出于处理器性能的原因。因此,即使是执行顺序互锁技巧在多线程环境中也可能不可靠。
如您所知,纠正此问题的主要工具是锁,它提供了一种独占访问机制(在同一锁的争用之间)并处理底层缓存同步,以便各种受锁保护的访问相同的内存位置代码部分将被正确序列化。您仍然可以在谁获得锁的时间和顺序之间存在竞争条件,但是当您可以保证锁定部分的执行是原子的(在该锁的上下文中)时,这通常更容易处理。
您可以在任何引用类型的实例上获得锁(例如,从 Object 继承,而不是像 int 或 enums 这样的值类型,而不是 null),但了解对象上的锁对访问没有内在影响是非常重要的对于该对象,它只与其他获取同一对象锁定的尝试进行交互。由类使用适当的锁定方案来保护对其成员变量的访问。有时,实例可能会通过锁定自身来保护对其自身成员的多线程访问(例如lock (this) { ... }
),但通常这不是必需的,因为实例往往只由一个所有者持有,并且不需要保证对实例的线程安全访问。
更常见的是,一个类创建一个私有锁(例如,private readonly object m_Lock = new Object();
为每个实例中的单独锁以保护对该实例的成员的访问,或private static readonly object s_Lock = new Object();
为一个中央锁来保护对该类的静态成员的访问)。Josh 有一个更具体的使用锁的代码示例。然后,您必须对类进行编码以适当地使用锁。在更复杂的情况下,您甚至可能希望为不同的成员组创建单独的锁,以减少对不一起使用的不同类型资源的争用。
所以,回到你原来的问题,一个只访问它自己的局部变量和参数的方法将是线程安全的,因为它们存在于当前线程特定的堆栈上它们自己的内存位置,并且不能被访问其他地方——除非你在传递它们之前跨线程共享这些参数实例。
仅访问实例自己的成员(无静态成员)的非静态方法——当然还有参数和局部变量——不需要在单个所有者使用的实例的上下文中使用锁(不需要需要是线程安全的),但是如果打算共享实例并希望保证线程安全访问,则该实例将需要使用特定于该实例的一个或多个锁来保护对其成员变量的访问(锁定实例本身是一种选择)——而不是让调用者在共享不打算线程安全共享的东西时围绕它实现自己的锁。
访问从未被操作过的只读成员(静态或非静态)通常是安全的,但如果它持有的实例本身不是线程安全的,或者如果您需要保证对它的多次操作的原子性,那么您可能需要也可以使用您自己的锁定方案来保护对它的所有访问。在这种情况下,如果实例对自身使用锁定,它可能会很方便,因为您可以简单地通过多次访问实例来获得对实例的锁定以实现原子性,但如果是单次访问,则不需要这样做对自身使用锁来使这些访问单独成为线程安全的。(如果不是您的班级,您必须知道它是自锁还是使用您无法从外部访问的私人锁。)
最后,可以从实例中访问不断变化的静态成员(由给定方法或任何其他方法更改)——当然还有访问这些静态成员并且可以从任何人、任何地点、任何时间调用的静态方法——它们有最需要使用负责任的锁定,没有它绝对不是线程安全的,并且可能会导致不可预知的错误。
在处理 .NET 框架类时,Microsoft 在 MSDN 中记录了给定的 API 调用是否是线程安全的(例如,所提供的通用集合类型的静态方法(例如)List<T>
是线程安全的,而实例方法可能不是——但要特别检查确定)。绝大多数时候(除非它明确说明它是线程安全的),它不是内部线程安全的,因此您有责任以安全的方式使用它。即使个别操作在内部实现线程安全,如果代码执行任何需要原子的更复杂的操作,您仍然需要担心代码的共享和重叠访问。
一个重要的警告是迭代集合(例如 with foreach
)。即使对集合的每次访问都获得了稳定状态,也不能内在保证它不会在这些访问之间发生变化(如果其他任何地方都可以访问它)。当集合在本地保存时,通常没有问题,但是可以更改的集合(由另一个线程或在循环执行期间!)可能会产生不一致的结果。解决此问题的一种简单方法是使用原子线程安全操作(在您的保护锁定方案内)来制作集合的临时副本(MyType[] mySnapshot = myCollection.ToArray();
) 然后遍历锁外的本地快照副本。在许多情况下,这避免了一直持有锁的需要,但是根据您在迭代中所做的事情,这可能还不够,您只需要一直防止更改(或者您可能已经拥有它在里面一个锁定的部分,防止访问更改集合以及其他东西,所以它被覆盖了)。
所以,线程安全设计有一点艺术,知道在哪里以及如何获得锁来保护事物在很大程度上取决于你的类的整体设计和使用。很容易变得偏执并认为您必须为所有事情都加锁,但实际上这是关于找到保护事物的正确层。