52

我正在为 ASP.NET 缓存项删除事件创建回调函数。

文档说我应该调用一个对象上的方法或我知道将存在的调用(将在范围内),例如静态方法,但它说我需要确保静态是线程安全的。

第 1 部分:我可以做哪些事情来使其成为非线程安全的?

第 2 部分:这是否意味着如果我有

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

我打电话给 Class.addOne(5); 和 Class.addOne(6);同时,根据哪个调用首先设置 foo,我可能会返回 6 或 7 吗?(即竞争条件)

4

11 回答 11

57

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. 
于 2009-01-07T16:16:21.733 回答
39

不,addOne 在这里是线程安全的——它只使用局部变量。这是一个不是线程安全的示例:

 class BadCounter
 {
       private static int counter;

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

在这里,两个线程可以同时调用 Increment,最终只增加一次。(return ++counter;顺便说一句,使用同样糟糕——上面是同一件事的更明确的版本。我扩展了它,所以它更明显是错误的。)

什么是线程安全和不是线程安全的细节可能非常棘手,但一般来说,如果你没有改变任何状态(除了传递给你的状态,无论如何 - 那里有点灰色区域),那么通常没问题.

于 2009-01-07T16:11:24.143 回答
22

线程问题(我最近也一直在担心)来自使用具有单独缓存的多个处理器内核,以及基本的线程交换竞争条件。如果不同内核的缓存访问相同的内存位置,它们通常不会知道另一个,并且可能会单独跟踪该数据位置的状态,而不会返回到主内存(甚至是在所有内存中共享的同步缓存)例如 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();) 然后遍历锁外的本地快照副本。在许多情况下,这避免了一直持有锁的需要,但是根据您在迭代中所做的事情,这可能还不够,您只需要一直防止更改(或者您可能已经拥有它在里面一个锁定的部分,防止访问更改集合以及其他东西,所以它被覆盖了)。

所以,线程安全设计有一点艺术,知道在哪里以及如何获得锁来保护事物在很大程度上取决于你的类的整体设计和使用。很容易变得偏执并认为您必须为所有事情都加锁,但实际上这是关于找到保护事物的正确层。

于 2009-01-07T19:14:04.213 回答
7

你的方法很好,因为它只使用局部变量,让我们稍微改变一下你的方法:

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 就没有意义了,但是嘿,这是一个示例。

于 2009-01-07T16:14:08.243 回答
4

正在进行一些研究,可让您检测非线程安全代码。例如微软研究院的国际象棋项目。

于 2009-01-07T16:26:38.077 回答
3

如果它正在修改函数外部的某些变量,这将只是一个竞争条件。你的例子没有这样做。

这基本上就是您要寻找的。线程安全意味着该函数:

  1. 不修改外部数据,或
  2. 对外部数据的访问已正确同步,因此任何时候只有一个功能可以访问它。

外部数据可以是存储中的东西(数据库/文件),也可以是应用程序内部的东西(变量、类的实例等):基本上是在函数范围之外的任何地方声明的任何东西。

您的函数的非线程安全版本的一个简单示例是:

private int myVar = 0;

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

如果您在没有同步的情况下从两个不同的线程调用它,则查询 myVar 的值会有所不同,具体取决于查询是在对 addOne 的所有调用完成之后发生,还是查询发生在两个调用之间,或者查询发生在任何一个之前电话。

于 2009-01-07T16:12:30.477 回答
2

在上面的例子中没有。

线程安全主要与存储状态有关。您可以通过执行以下操作使上述示例成为非线程安全的:

static int myInt;

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

这意味着由于上下文切换线程 1 可能会调用 myInt = someNumber 然后进行上下文切换,假设线程 1 只是将其设置为 5。然后想象线程 2 进来并使用 6 并返回 7。然后当线程1 再次唤醒它将在 myInt 中有 6 而不是它正在使用的 5 并返回 7 而不是预期的 6。:O

于 2009-01-07T16:14:35.403 回答
1

在任何地方,线程安全意味着您在访问资源时不会有两个或更多线程发生冲突。通常静态变量——在 C#、VB.NET 和 Java 等语言中——使你的代码线程不安全

在 Java 中存在synchronized关键字。但是在 .NET 中,您可以获得程序集选项/指令:


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

非线程安全类的示例应该是单例,具体取决于此模式的编码方式。通常它必须实现一个同步的实例创建器。

如果不想要同步方法,可以尝试加锁方法,比如自旋锁

于 2009-01-07T16:41:38.093 回答
0

对两个线程可以同时使用的对象的任何访问都不是线程安全的。

您在第 2 部分中的示例显然是安全的,因为它仅使用作为参数传入的值,但如果您使用对象范围的变量,您可能必须用适当的锁定语句包围访问

于 2009-01-07T16:11:43.787 回答
0

foo在并发或顺序调用之间不共享,因此addOne是线程安全的。

于 2009-01-07T16:11:50.430 回答
0

在您的示例中 'foo' 和 'someNumber' 安全的原因是它们驻留在堆栈上,并且每个线程都有自己的堆栈,因此不共享。

一旦数据有可能被共享,例如,作为全局或共享指向对象的指针,那么您可能会发生冲突并且可能需要使用某种类型的锁。

于 2009-01-07T16:17:57.297 回答