c# - 当本地 stackalloc 上的数据时,匿名委托(delegate)不会在每次迭代中使用新本地

标签 c# .net-core threadpool anonymous-delegates stackalloc

在 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# 不跟踪关于匿名 delegatestackalloc 空间?我知道 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/

相关文章:

multithreading - Delphi - OTL - ThreadPool 和 Worker 线程之间的通信

java - java ThreadPool 中的取消函数 - 检测池中的特定可运行对象并将其取消

c# - 使用自定义成员资格提供程序扩展 MembershipUser 类

ubuntu - dotnet build obj/project.assets.json' 已经存在

c# - 使用 .Net Core 的 T4 参数指令

.net - 尝试将 AutoMapper 添加到 .NetCore1.1 - 无法识别 services.AddAutoMapper()

python - 我怎样才能让这两个线程一个接一个地不断运行python?

c# - 了解SignalR的加载过程

c# - 构建 Azure Function v3 时无法解析程序集 'Microsoft.AspNetCore.Mvc.Core, Version=3.1.0.0"

c# - 如何将人们的 STRAVA 个人资料与我的网站相关联?