在C#中使用匿名委托时,CLR将在堆上为已使用的变量生成本地副本(例如,当前作用域中的变量).对于当前作用域的每个声明变量,这样的local将被放到堆上.
您可以在此示例中看到此行为:
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
使用常规局部变量时 – 只需替换上面示例中的调用方法:
static void call(int number)
{
int j = number;
ThreadPool.QueueUserWorkItem(delegate { execute(j); });
}
输出:
* NUM=0
* NUM=1
* NUM=2
* NUM=3
* NUM=4
这种情况使我不相信C#中的匿名代表 – 因为我不明白C#什么时候不会搞砸我对匿名代表的调用.
我的问题:为什么C#没有跟踪匿名代表的stackalloc空间?我知道C#没有跟踪.我想知道为什么它不跟踪,如果它与常规变量一起.
对于这些示例,我使用带有C#7.3的.NET Core 2.1包括/ unsafe开关.
解决方法:
问题是你正在捕捉一个指针.该指针指的是通过调用在堆栈上分配的内存 – 即使在方法返回后指针也会保留对它的引用,这从根本上是坏消息.那时你正处于未定义的领域 – 不能保证以后在那个记忆中会有什么.
每个stackalloc确实单独出现 – 你得到的五个指针都是独立的,但是它们碰巧引用同一块内存,因为每个指针都是堆栈指针处于相同值时开始执行的单独stackalloc执行的结果.你仍然可以使用那个内存,因为它在进程中仍然是有效的内存,但是在知道将要存在的内容方面这样做是不安全的.
ints变量被“正确”复制到编译器生成的类中,但变量的值指的是在调用call方法时堆栈上的内存.当我运行代码时,我输出“无论Thread.Sleep的参数是什么.C#编译器正在捕获变量,这与”捕获堆栈的整个内容“不同.
您不需要完全避免代理 – 您只需要避免将代理与不安全的代码和堆栈分配混合在一起.
您可以在不使用任何匿名函数的情况下看到此问题:
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]);
}
}
}
你可以在不使用任何委托的情况下轻松地看到它,但是做同样的事情“堆栈分配一些内存,并在堆栈消失后使用指向它的指针”:
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;
}
}