我怎么知道这个C#方法是线程安全的?
我正在创build一个ASP.NETcaching项删除事件的callback函数。
文档说,我应该调用一个对象的方法或我知道会存在的调用(将在范围内),如静态方法,但它说我需要确保静态是线程安全的。
第一部分:我可以做些什么事情让事情变得不安全?
第2部分:这是否意味着,如果我有
static int addOne(int someNumber){ int foo = someNumber; return foo +1; }
我叫Class.addOne(5); 和Class.addOne(6); 同时,我可能得到6或7返回取决于哪些调用集foo第一? (即比赛条件)
addOne函数确实是线程安全的,因为它不访问任何可以被另一个线程访问的数据。 局部variables不能在线程间共享,因为每个线程都有自己的栈。 但是,您必须确保函数参数是值types而不是引用types。
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.
不,addOne在这里是线程安全的 – 它只使用局部variables。 这是一个不会线程安全的例子:
class BadCounter { private static int counter; public static int Increment() { int temp = counter; temp++; counter = temp; return counter; } }
在这里,两个线程可以同时调用Increment,最后只增加一次。 (使用return ++counter;
顺便说一句,上面是同一个东西的更明确的版本,所以我把它扩展了,所以显然是错误的)。
什么是和不是线程安全的细节可能是非常棘手的,但总的来说,如果你没有改变任何状态(除了传入你的东西,无论如何 – 有点灰色地带),那么通常是好的。
线程问题(我最近也一直担心)是由于使用了多个独立的caching的处理器核心,以及基本的线程交换竞争条件。 如果单独内核的高速caching访问相同的内存位置,他们通常不知道另一个内存位置,可能会单独跟踪数据位置的状态,而不会返回到主内存(甚至是跨所有共享的同步高速caching例如,在L2或L3上的核心)出于处理器性能的原因。 所以即使是在multithreading环境中,执行顺序互锁技巧也可能不可靠。
正如您所知,纠正这个问题的主要工具是一个锁,它提供了一个独占访问机制(在同一个锁的争用之间)并处理底层的高速caching同步,以便通过各种锁保护访问相同的内存位置代码段将被正确地序列化。 你仍然可以在谁获得锁的时间和顺序之间存在竞争条件,但是当你可以保证locking部分的执行是primefaces的时候(在该锁的上下文中),通常要处理得更简单。
你可以locking任何引用types的实例(例如从Objectinheritance,而不是像int或枚举这样的值types,而不是null),但是理解对象的锁对访问没有固有的影响是非常重要的对于那个对象,它只与其他尝试获取对同一个对象的locking相互作用。 这是由类保护访问其成员variables使用适当的lockingscheme。 有时候,实例可以通过locking自己(例如lock (this) { ... }
)来保护对自己成员的multithreading访问,但通常这不是必须的,因为实例往往只由一个所有者持有,需要保证对实例的线程安全访问。
更常见的是,一个类创build一个私有锁(例如, private readonly object m_Lock = new Object();
用于每个实例中的单独锁以保护对该实例成员的访问,或private static readonly object s_Lock = new Object();
for一个中央锁以保护对该类静态成员的访问)。 乔希有一个更具体的使用锁的代码示例。 然后你必须编写这个类来适当地使用这个锁。 在更复杂的情况下,您甚至可能希望为不同的成员组创build单独的锁,以减less不一起使用的各种资源的争用。
所以,为了回到你原来的问题,一个只访问它自己的局部variables和参数的方法是线程安全的,因为它们存在于当前线程特定的栈中它们自己的内存位置上,并且不能被访问其他地方 – 除非在通过线程之前在线程之间共享这些参数实例。
只访问实例自己成员(没有静态成员)的非静态方法 – 当然还有参数和局部variables – 不需要在单个所有者使用的实例上下文中使用锁需要是线程安全的),但是如果实例想要被共享,并希望保证线程安全的访问,那么实例将需要保护对其成员variables的访问,使用一个或多个特定于该实例的锁(locking实例本身就是一个选项) – 而不是让它在调用者自己的周围实现它们的锁时,共享某些不是线程安全共享的东西。
访问只读成员(静态的或非静态的)从来没有被操纵的通常是安全的,但是如果它所持有的实例本身不是线程安全的,或者如果你需要通过多次操作来保证primefaces性,那么你可能需要以保护您自己的lockingscheme的所有访问权限。 这种情况下,如果实例使用自己的locking,可能会非常方便,因为您可以简单地通过多次访问将实例locking为primefaces性,但是如果它是单向访问,则不需要这样做使用自己的锁使这些访问单独线程安全。 (如果不是你的class级,你必须知道自己是否locking,或者正在使用一个你无法访问的私人锁。)
最后,可以在一个实例中访问更改静态成员(由给定方法或其他方法更改) – 当然还有访问这些静态成员的静态方法,并且可以随时随地从任何人调用 – 具有最需要使用负责任的locking,否则绝对不是线程安全的,并可能导致不可预知的错误。
在处理.NET框架类时,MSDN中的Microsoft文档是否给定的API调用是线程安全的(例如,所提供的通用集合types的静态方法(如List<T>
是线程安全的,而实例方法可能不是 -但具体检查确定)。 绝大多数时间(除非它明确表示它是线程安全的),它不是内部线程安全的,所以您有责任以安全的方式使用它。 即使单个操作是在内部实现线程安全的,但如果需要进行更复杂的操作,您仍然需要担心代码的共享和重叠访问。
一个重要的警告是迭代一个集合(例如与foreach
)。 即使对集合的每个访问都获得一个稳定的状态,也不存在在这些访问之间不会改变的固有保证(如果其他地方能够达到的话)。 当集合在本地进行时通常没有问题,但是可以更改的集合(通过另一个线程或在循环执行期间)可能会产生不一致的结果。 解决这个问题的一个简单方法是使用primefaces线程安全操作(在您的保护lockingscheme内)来创build集合的临时副本( MyType[] mySnapshot = myCollection.ToArray();
),然后遍历该本地快照在锁外复制。 在很多情况下,这样做可以避免在整个过程中保持一个锁,但是根据你在迭代过程中做什么,这可能是不够的,你只需要防止整个时间的变化(或者你可能已经在里面一个locking的部分防止访问改变集合与其他东西,所以它被覆盖)。
所以,线程安全devise有一些技巧,而且知道在哪里以及如何获得锁来保护事物取决于你的类的总体devise和用法。 可能很容易出现偏执狂,认为你必须把所有的东西都锁住,但其实是要find合适的层来保护东西。
你的方法是好的,因为它只使用局部variables,让我们稍微改变你的方法:
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; }
我认为这是一个愚蠢的例子,如果我正确地阅读它,那么就没有意义了,但是嘿,这只是一个例子。
有一些研究正在进行,它允许你检测非线程安全的代码。 例如微软研究院的CHESS项目。
这只会是一个竞争条件,如果它正在修改某些variables外部的function。 你的例子没有这样做。
这基本上是你正在寻找的。 线程安全意味着该function要么是:
- 不修改外部数据或
- 对外部数据的访问已正确同步,因此任何时候只有一个function可以访问它。
外部数据可以是存储在数据库/文件中的东西,也可以是应用程序内部的东西(variables,类的实例等):基本上任何在函数范围之外的世界任何地方声明的东西。
你的函数的非线程安全版本的一个简单的例子是:
private int myVar = 0; private void addOne(int someNumber) { myVar += someNumber; }
如果你从两个不同的线程调用这个而没有同步,那么查询myVar的值将会有所不同,这取决于查询是在所有对addOne的调用完成之后发生的,还是查询发生在两个调用之间,或者查询发生在电话。
在上面的例子中,
线程安全主要是与存储状态有关。 你可以通过这样做使上面的例子非线程安全:
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
任何地方, 线程安全意味着当你访问一个资源时你没有两个或者更多的线程发生冲突。 通常静态variables – 像C#,VB.NET和Java这样的语言 – 使得你的代码线程不安全 。
在Java中存在synchronized关键字。 但在.NET中,您可以获得组装选项/指令:
class Foo { [MethodImpl(MethodImplOptions.Synchronized)] public void Bar(object obj) { // do something... } }
非线程安全类的例子应该是单例 ,这取决于这个模式如何编码。 通常它必须实现一个同步的实例创build者。
如果你不想要一个同步的方法 ,你可以尝试locking方法,比如旋转locking 。
任何对两个线程同时使用的方法的访问都不是线程安全的。
第2部分中的示例显然是安全的,因为它仅使用作为参数传入的值,但如果使用了对象作用域variables,则可能必须使用适当的locking语句
foo
不是在并发调用和顺序调用之间共享的,所以addOne
是线程安全的。
在你的例子中,'foo'和'someNumber'是安全的原因是它们驻留在堆栈上,每个线程都有自己的堆栈,所以不共享。
只要数据具有共享的潜力,例如全局的或共享指向对象的指针,那么就可能发生冲突,可能需要使用某种types的锁。