在 C# 中使用匿名 delegate
时,CLR 将在堆上为使用过的变量生成局部副本(例如,当前作用域中的变量)。对于当前作用域的每个已声明变量,这样的局部变量将被放入堆中。
你可以在这个例子中看到这个行为:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
ThreadPool.QueueUserWorkItem(delegate { execute(i); });
Thread.Sleep(1000);
Console.WriteLine();
for (int i = 0; i < 5; i++)
{
int j = i;
ThreadPool.QueueUserWorkItem(delegate { execute(j); });
}
Thread.Sleep(1000);
}
static void execute(int number)
{
Console.WriteLine(" * NUM=" + number.ToString());
}
}
此程序的输出是(最后 5 个条目的顺序可能有所不同,而第一个条目的顺序也可能小于 5。):
* NUM=5
* NUM=5
* NUM=5
* NUM=5
* NUM=5
* NUM=0
* NUM=1
* NUM=2
* NUM=3
* NUM=4
在方法中调用时,C# 应始终生成本地的新副本。这在本示例中按预期工作:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
call(i);
Thread.Sleep(1000);
}
static void call(int number)
{
ThreadPool.QueueUserWorkItem(delegate { execute(number); });
}
static void execute(int number)
{
Console.WriteLine(" * NUM=" + number.ToString());
}
}
输出:
* NUM=0
* NUM=1
* NUM=2
* NUM=3
* NUM=4
这是有问题的情况:但是,当将变量分配给 stackalloc
保留区域时,它不工作:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
call(i);
Thread.Sleep(1000);
}
static unsafe void call(int number)
{
int* ints = stackalloc int[64];
ints[32] = number;
ThreadPool.QueueUserWorkItem(delegate { execute(ints[32]); });
}
static void execute(int number)
{
Console.WriteLine(" * NUM=" + number.ToString());
}
}
输出:
* NUM=4
* NUM=4
* NUM=4
* NUM=4
* NUM=4
当使用常规局部变量时 - 只需替换上面示例中的 call
方法:
static void call(int number)
{
int j = number;
ThreadPool.QueueUserWorkItem(delegate { execute(j); });
}
输出:
* NUM=0
* NUM=1
* NUM=2
* NUM=3
* NUM=4
这种情况让我不相信 C# 中的匿名 delegate
- 因为我不明白 C# 到底什么时候不会搞砸我对匿名 delegate
的调用。
我的问题:为什么 C# 不跟踪关于匿名 delegate
的 stackalloc
空间?我知道 C# 不保留追踪。我想知道为什么它不跟踪,如果它使用常规变量的话。
我将 .NET Core 2.1 与 C# 7.3 结合使用,包括用于这些示例的 /unsafe
开关。
最佳答案
问题是您正在捕获一个指针。该指针指向由 call
在堆栈上分配的内存 - 即使在方法返回后指针保持对它的引用,这从根本上来说是个坏消息。那时您进入了未定义的领域 - 无法保证以后该内存中会有什么。
每个 stackalloc
确实 分别出现 - 你得到的五个指针都是独立的,但它们发生指向同一 block 内存,因为每个都是单独的 stackalloc
执行的结果当堆栈指针处于相同的值开始时。您仍然可以使用该内存,因为它在进程中仍然是有效内存,但这样做不安全,因为要知道那里会发生什么。
ints
变量被“正确地”复制到由编译器生成的类中,但是变量的值 指的是位于堆栈上的内存调用 call
方法的时间。当我运行代码时,我得到了“无论 Thread.Sleep
的参数是什么。C# 编译器正在捕获变量,这与“捕获堆栈的全部内容”。
您不需要完全避免委托(delegate) - 您只需要避免将委托(delegate)与不安全的代码和堆栈分配混合在一起。
根本不用任何匿名函数也能看到这个问题:
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Call(i);
}
Thread.Sleep(999);
}
static unsafe void Call(int number)
{
Helper helper = new Helper();
int* tmp = stackalloc int[64];
helper.ints = tmp;
helper.ints[32] = number;
ThreadPool.QueueUserWorkItem(helper.Method);
}
static void Execute(int number)
{
Console.WriteLine(" * NUM=" + number.ToString());
}
unsafe class Helper
{
public int* ints;
public void Method(object state)
{
Execute(ints[32]);
}
}
}
无需使用任何委托(delegate),您就可以很容易地看到它,但是做同样的事情“堆栈分配一些内存,并在该堆栈消失后使用指向它的指针”:
using System;
class Program
{
unsafe static void Main(string[] args)
{
int*[] pointers = new int*[5];
for (int i = 0; i < 5; i++)
{
pointers[i] = Call(i);
}
for (int i = 0; i < 5; i++)
{
Console.WriteLine(pointers[i][32]);
}
}
unsafe static int* Call(int number)
{
int* ints = stackalloc int[64];
ints[32] = number;
return ints;
}
}
关于c# - 当本地 stackalloc 上的数据时,匿名委托(delegate)不会在每次迭代中使用新本地,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54414317/