繁体   English   中英

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

[英]Why can a local variable be accessed in another thread created in the same class?

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

根据我对.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()不应该.NET抛出异常,因为创建的对象mc运行在与mc -initializer中创建的线程不同的线程中,并且这个新线程正在尝试访问mc的局部变量?

MessageBox显示10 (不)预期,但我不知道为什么这应该工作。

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

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

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

您可能在某处读过不能在不同线程上访问局部变量的地方。 该陈述也是错误的,但通常是陈述。 正确的说法是那些不是的局部变量

  • 在异步方法中
  • 在迭代器块中(即具有yield returnyield break
  • 封闭的匿名函数的外部变量

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

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

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

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

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

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

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

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

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

你能解释一下STA和MTA吗?

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

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

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

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

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

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

我有点晚了,@ 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 -你的呼叫线程safe.aspx

不,你有它倒退,只要它仍然在范围内,数据是可访问的。

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

阅读.NET中的C#Threading以获得教程。

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

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

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

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

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

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

所以总结一下要点:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM