为什么可以在同一个类中创建的另一个线程中访问局部变量?

时间:2013-09-12 17:10:08

标签: c# multithreading local-variables

我真的找不到关于这个确切主题的任何内容,所以如果问题已经存在,请引导我走向正确的方向。

根据我对.NET的了解,不可能跨不同的线程访问变量(如果该语句错误,请纠正我,这就是我在某处读到的内容)。

然而,在这个代码示例中,它似乎不应该起作用:

class MyClass
{
    public int variable;

    internal MyClass()
    {
        Thread thread = new Thread(new ThreadStart(DoSomething));
        thread.IsBackground = true;
        thread.Start();
    }

    public void DoSomething()
    {
        variable = 0;
        for (int i = 0; i < 10; i++)
            variable++;

        MessageBox.Show(variable.ToString());
    }
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void SomeMethod();
    {
        MyClass mc = new MyClass();
    }
}

当我运行SomeMethod()时,不应该抛出异常,因为创建的对象mc运行在与mc - 初始化程序中创建的线程不同的线程中新线程正在尝试访问mc的本地变量?

MessageBox显示10为(非)预期,但我不确定为什么这应该有用。

也许我不知道要搜索什么,但我找不到任何线程主题,会解决这个问题,但也许我对变量和线程的看法是错误的。

5 个答案:

答案 0 :(得分:47)

  

根据我对.NET的了解,不可能跨不同的线程访问变量。如果该陈述错误,请纠正我,这正是我在某处读到的。

这句话完全是假的,所以请考虑一下你的修正。

您可能在某处读过不能在不同线程上访问局部变量的地方。该声明也是 false,但通常会说明。正确的陈述是不是

的局部变量
  • in async method
  • 迭代器块中的
  • (即具有yield returnyield break的方法)
  • 关闭匿名函数的外部变量
多个线程无法访问

。甚至那个说法有点狡猾;有方法可以用指针和unsafe代码块来做到这一点,但尝试这样做是一个非常糟糕的主意。

我还注意到您的问题询问了局部变量,但后来给出了字段的示例。根据定义,字段是局部变量。根据定义,局部变量是方法体的本地变量。 (或构造函数体,索引器主体等)确保您清楚。本地的定义特征不是它“在堆栈中”或某种类似的东西;本地的“本地”部分是其名称在方法体之外没有意义。

在一般情况下:变量是存储位置,引用内存。线程是进程中的控制点,进程中的所有线程共享相同的内存;这就是使他们线程而不是进程的原因。因此,通常情况下,所有变量都可以由多个线程随时以及所有顺序访问,除非采用某种机制来防止

让我再说一遍,只是为了确保它在你的脑海中绝对清晰:考虑单线程程序的正确方法是所有变量都是稳定的,除非有些东西让它们改变。考虑多线程程序的正确方法是所有变量无特定顺序中不断变异,除非某些变量保持静止或有序。 这是多线程共享内存模型如此困难的根本原因,因此你应该避免使用它。

在您的特定示例中,两个线程都可以访问this,因此两个线程都可以看到变量this.variable。您没有实现任何机制来阻止这种情况,因此两个线程都可以按任何顺序写入和读取该变量,但实际上受到的限制非常少。您可以实现以驯服此行为的一些机制是:

  • 将变量标记为ThreadStatic。这样做会导致在每个线程上创建一个新变量。
  • 将变量标记为volatile。这样做会对可能观察到的顺序读取和写入施加某些限制,并对编译器或CPU可能导致意外结果的优化施加某些限制。
  • 在变量的每次使用周围加上lock语句。
  • 首先不要共享变量。

除非您对深入了解多线程和处理器优化,否则我建议不要使用除后者之外的任何选项。

现在,假设您确实希望确保在另一个线程上对变量的访问失败。您可以让构造函数捕获创建线程的线程ID并将其存储起来。然后,您可以通过属性getter / setter访问该变量,其中getter和setter检查当前线程ID,如果它与原始线程ID不同,则抛出异常。

基本上它的作用是推出你自己的单线程公寓线程模型。 “单线程单元”对象是一个只能在创建它的线程上合法访问的对象。 (你买一台电视,你把它放在你的公寓里,只允许你公寓里的人看你的电视。)单线程公寓与多线程公寓和免费线程的细节相当复杂;有关更多背景,请参阅此问题。

Could you explain STA and MTA?

这就是为什么,例如,您必须永远不能从工作线程访问您在UI线程上创建的UI元素; UI元素是STA对象。

答案 1 :(得分:8)

  

根据我对.NET的了解,不可能跨不同的线程访问变量(如果该语句错误,请纠正我,这就是我在某处读到的内容)。

这是不正确的。可以从范围内的任何位置访问变量。

从多个线程访问同一个变量时需要谨慎,因为每个线程都可以在非确定性时间对变量进行操作,从而导致细微的,难以解决的错误。

有一个出色的网站,涵盖从基础知识到高级概念的.NET中的线程。

http://www.albahari.com/threading/

答案 2 :(得分:5)

我有点晚了,@ Eric J.给出的答案非常精彩而且非常重要。

我只想在你对线程和变量的看法中为另一个问题添加一些清晰度。

你在问题​​的标题中说过“变量可以在另一个帖子中访问”。 除此之外,在您的代码中,您正在从 1个线程中访问您的变量,这是在此处创建的线程:

    Thread thread = new Thread(new ThreadStart(DoSomething));
    thread.IsBackground = true;
    thread.Start();

所有这些让我意识到你害怕与实际创建MyClass实例的线程不同的线程将使用该实例内部的东西。

以下事实对于更清楚地了解多线程是什么非常重要(它比您想象的更简单):

  • 线程不拥有变量,它们拥有堆栈,堆栈可能包含一些变量,但这不是我的观点
  • 创建类实例的线程与该线程之间没有内在联系。它由所有线程拥有,就像它们不属于任何线程一样。
  • 当我说这些事情时,我不是在谈论线程堆栈,但有人可能会说线程和实例是两组独立的对象,只是为了更大的利益而进行交互:)

编辑

我看到线程安全这个词出现在这个答案的主题上。 万一你可能想知道这些词是什么意思我推荐这篇由@Eric Lippert撰写的精彩文章: http://blogs.msdn.com/b/ericlippert/archive/2009/10/19/what-is-this-thing-you-call-thread-safe.aspx

答案 3 :(得分:2)

不,你有倒退,只要数据仍在范围内,就可以访问数据。

你需要防范相反的问题,两个线程同时访问相同的数据,这称为竞争条件。您可以使用lock之类的同步技术来防止这种情况发生,但如果使用不当,可能会导致死锁。

阅读C# Threading in .NET获取教程。

答案 4 :(得分:2)

内存位置不会隔离到单个线程。如果它们真的很不方便。 CLR中的内存仅在application domain边界处被隔离。这就是每个AppDomain每个静态变量都有一个单独实例的原因。但是,threads are not tied to any one particular application domain。它们可以在多个应用程序域中执行代码,也可以不执行任何代码(非托管代码)。他们不能做的是同时从多个应用程序域执行代码。这意味着线程无法同时访问来自两个不同应用程序域的数据结构。这就是为什么你必须使用编组技术(例如通过MarshalByRefObject)或使用.NET Remoting或WCF等通信协议来访问另一个应用程序域的数据结构。

考虑以下用于托管CLR的进程的unicode art图。

┌Process───────────────────────────────┐
│                                      │
│ ┌AppDomain───┐        ┌AppDomain───┐ │
│ │            │        │            │ │ 
│ │       ┌──────Thread──────┐       │ │
│ │       │                  │       │ │
│ │       └──────────────────┘       │ │
│ │            │        │            │ │
│ └────────────┘        └────────────┘ │
└──────────────────────────────────────┘

您可以看到每个进程可以有多个应用程序域,并且一个线程可以从多个进程中执行代码。我还尝试说明一个线程也可以通过在左右AppDomain块之外显示它的存在来执行非托管代码。

所以基本上一个线程对当前正在执行的同一个应用程序域中的任何数据结构都有简单而重要的访问。我在这里使用术语“琐碎”来包含内存访问(数据结构,变量等) 。)通过公共,受保护或内部成员从一个班级到另一个班级。线程决不会阻止这种情况发生。但是,使用反射,您甚至可以访问另一个类的私有成员。这就是我所说的非平凡访问。是的,它涉及到你自己的一些工作,但是一旦你完成了反射调用就没有任何花哨的东西了(顺便提一下,代码访问安全性必须允许这样做,但这是一个不同的主题)。关键是一个线程可以访问它正在执行的同一个应用程序域中的几乎所有内存。

线程可以访问同一应用程序域中的几乎所有内容的原因是因为如果它没有,那将是非常严格的限制。在多线程环境中工作时,开发人员必须付出额外的努力来在类之间共享数据结构。

总结一下要点:

  • 数据结构(类/结构)或其组成成员与线程之间没有一对一的关系。
  • 线程与应用程序域之间没有一对一的关系。
  • 从技术上讲,OS线程和CLR线程之间甚至没有一对一的关系(尽管实际上我知道没有偏离该方法的CLI的主流实现 1 )。
  • 显然,CLR线程仍然局限于创建它的过程。

1 即使Singularity operating system似乎也直接将.NET线程映射到操作系统和硬件。