.NET中的多线程和闭包

12

如果我有这个:

public string DoSomething(string arg)
{
    string someVar = arg;
    DoStuffThatMightTakeAWhile();
    return SomeControl.Invoke(new Func<string>(() => someVar));
}

如果有多个线程同时调用此方法,并且一个线程在DoStuffThatMightTakeAWhile处被阻塞,然后第二个线程以不同的arg调用了DoSomething,那么这是否会更改someVar的值并因此使得DoSomething在两次调用时都返回第二个版本的someArg,还是每个线程都存在一个someVar

编辑:我认为我的Action应该是一个Func,所以进行了修改。

3个回答

17
这里的答案有一些混淆,主要基于局部变量是分配在“线程堆栈”上的不真实说法。这是错误的和无关紧要的。它是错误的,因为局部变量可能分配在某些临时池或长期存储池中;即使它分配在临时池中,也不一定是堆栈内存;它可以是寄存器。它是无关紧要的,因为谁在乎存储分配在哪个池上?
相关事实是:每个方法激活都会分配一个顶层局部变量。更普遍地说,在块中声明的局部变量每次进入该块时都会分配一次;例如,在循环体中声明的局部变量每次循环时都会被分配。
因此,让我们考虑您的问题:
此方法可以从多个线程并发调用。如果一个线程卡在DoStuffThatMightTakeAWhile,然后第二个线程使用不同的参数调用DoSomething,这是否会更改someVar的值,使所有线程的someVar值都改变?
不会。对于DoSomething的每个激活都会有一个新的"someVar"。
每个线程会存在一个someVar吗?
每个激活将存在一个“someVar”。如果一个线程只执行一个激活,则每个线程将存在一个someVar;如果一个线程执行一百万个激活,则将存在一百万个someVar。
尽管如此,Jon Hanna的答案也是正确的:如果你创建一个委托,既读取又写入局部变量,并将该委托分配给多个线程,那么委托的所有激活都共享相同的局部变量。对于您不会自动创建神奇的线程安全性,如果您要这样做,则需要确保线程安全。

简单来说,像string myVar;这样的声明将会 创建 一个名为myVar的新变量,就像使用new关键字创建对象一样。如果DoSomething调用DoSomething,这个规则也适用吗? - Juan
2
@jsoldi:内存分配器创建新局部变量和为对象创建新存储的方式在幕后可能非常不同,但从概念上讲它们是相同的。而且,递归调用与非递归调用一样都是“激活”。每个“激活”都会获得一批新的局部变量。 - Eric Lippert
1
为什么你把这称为“方法激活”而不是“方法调用”?这是一个专用术语吗? - Restuta

7

本地变量存储在当前线程的堆栈中,因此每个线程都有自己的堆栈和其中的someVar变量。

由于每个线程中的值都不同,因此它们是各自独立的。

new Action(() => someVar)); 

将捕获其自身的someVar值。

编辑

我之前的说法是错误的,正如Eric所指出的那样。请参见他的答案获取正确的解释。


1
说“局部变量存储在当前线程的堆栈上”是不正确的。任何在闭包中被捕获的变量都分配在堆上。 - Jason Malinowski
Jason是正确的。这个答案没有合理的逻辑。 - Eric Lippert
是的,我的错,我不会修改我的答案,因为你的答案已经非常好了。 - Restuta

1
如上所述,每个调用DoSomething的线程都会在其堆栈上创建一个单独的someVar,因此没有线程对另一个线程的someVar产生任何影响。
值得注意的是,如果在闭包中捕获了本地变量并且在该范围内启动了多线程,则这确实会导致不同的线程影响彼此看到的值,即使在值类型的情况下(我们通常认为另一个方法无法影响值类型 - 在这种方式下,闭包不像类方法:
public static void Main(string[] args)
{
    int x = 0;
    new Thread(() => {while(x != 100){Console.WriteLine(x);}}).Start();
    for(int i = 0; i != 100; ++i)
    {
        x = i;
        Thread.Sleep(10);
    }
    x = 100;
    Console.ReadLine();
}

演示这个。


如果我理解正确,在您的示例中只有一个 x(在主线程中创建的)被每个线程访问了? - Juan
确切地说,手动创建的线程将其写入控制台,而主线程则对其进行递增。 - Jon Hanna
1
请注意,someVar在第一次分配时并未分配在堆栈上。闭包局部变量分配在堆上,因为它们的生命周期被延长。 - Eric Lippert
没错。@EricLippert 简洁地表达了我发帖时无法准确表述的内容。实际上,我的第一句话错误地表达了相反的意思,尽管在这种情况下生命周期没有延长,但就多线程而言逻辑上仍然相同,我的观点是有时仍然适用于局部变量的只有一个线程可以看到的推理,有时则不适用。 - Jon Hanna

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接