15

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

根据我对 .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在与 -initializer 中创建的线程不同的线程中运行,mc并且这个新线程正在尝试访问mc?

MessageBox节目如(不是)预期的10那样,但我不确定为什么这应该起作用。

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

4

5 回答 5

54

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

该陈述是完全错误的,因此请将此视为您的更正。

您可能在某处读到不能跨不同线程访问局部变量。该陈述也是错误的,但通常被陈述。正确的说法是局部变量不是

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

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

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

在一般情况下:变量是引用内存的存储位置。线程是进程中的一个控制点,进程中的所有线程共享相同的内存;这就是使它们成为线程而不是进程的原因。所以一般来说,所有变量都可以在任何时候被多个线程以所有顺序访问,除非有某种机制来防止这种情况发生

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

在您的特定示例中,两个线程都可以访问this,因此两个线程都可以看到变量this.variable。您没有实施任何机制来防止这种情况发生,因此两个线程都可以以任何顺序写入和读取该变量,实际上几乎没有限制。您可以实施一些机制来控制这种行为:

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

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

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

本质上,它所做的是滚动您自己的单线程单元线程模型。“单线程单元”对象是只能在创建它的线程上合法访问的对象。(你买了一台电视,你把它放在你的公寓里,只有你公寓里的人才能看你的电视。)单线程公寓、多线程公寓和自由线程的细节变得相当复杂。有关更多背景信息,请参阅此问题。

你能解释一下STA和MTA吗?

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

于 2013-09-12T20:02:42.097 回答
8

From what I have learned about .NET, it is not possible to access variables across different threads (please correct me if that statement is wrong, it's just what I have read somewhere).

That is not correct. A variable can be accessed from anywhere that it is in scope.

You need to exercise caution when accessing the same variable from multiple threads because each thread can act on the variable at a non-deterministic time leading to subtle, hard-to-resolve bugs.

There is an outstanding website that covers threading in .NET from the basics to advanced concepts.

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

于 2013-09-12T17:12:33.310 回答
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

于 2013-09-12T17:23:46.020 回答
2

不,你把它倒过来了,只要数据还在范围内,它就可以访问。

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

阅读.NET 中的 C# 线程以获取教程。

于 2013-09-12T17:14:08.013 回答
2

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

考虑以下托管 CLR 的进程的 unicode 艺术图。

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

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

所以基本上一个线程对它当前正在执行的同一个应用程序域中的任何数据结构都有微不足道和非微不足道的访问。我在这里使用术语“微不足道”来包括内存访问(数据结构、变量等)通过从一个类到另一个类的公共、受保护或内部成员。线程绝不会阻止这种情况的发生。然而,使用反射你仍然可以访问甚至另一个类的私有成员。这就是我所说的非平凡访问。是的,这需要您做更多的工作,但是一旦您完成了反射调用(顺便说一句,代码访问安全性必须允许,但这是另一个主题),就没有什么特别的事情发生了。

一个线程可以访问同一个应用程序域中几乎所有内容的原因是因为如果它没有,它会受到疯狂的限制。在多线程环境中工作时,开发人员必须付出很多额外的努力才能在类之间共享数据结构。

所以总结一下重点:

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

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

于 2013-09-12T19:40:32.857 回答