4

在 C# 中使用匿名delegates 时,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

这种情况使我不信任delegateC# 中的匿名 s - 因为我不明白 C# 何时不会搞砸我对匿名delegates 的调用。

我的问题:为什么 C# 不跟踪stackalloc有关匿名delegates 的空间?我知道 C# 没有跟踪。我想知道为什么它不跟踪,如果它使用常规变量。

我使用 .NET Core 2.1 和 C# 7.3,包括/unsafe这些示例的开关。

4

1 回答 1

6

问题是您正在捕获指针。该指针指的是由call- 在堆栈上分配的内存,即使在方法返回后,指针也会保持对它的引用,这从根本上来说是个坏消息。那时你进入了未定义的领域——无法保证以后的记忆中会出现什么。

每个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;
    }
}
于 2019-01-29T07:21:46.453 回答