在C#中检查堆栈大小

Gjo*_*gji 15 c# stack multithreading

有没有办法在C#中检查线程堆栈大小?

Ore*_*ner 17

这是一个例子,如果你不得不问,你买不起(Raymond Chen先说.)如果代码依赖于有足够的堆栈空间到必须首先检查的程度,那么它可能是值得的重构它以使用显式Stack<T>对象.约翰关于使用分析器的评论是有用的.

也就是说,事实证明有一种方法可以估算剩余的堆栈空间.它不精确,但它足够有用,可以评估你的底部有多接近底部.以下内容主要基于Joe Duffy撰写精彩文章.

我们知道(或将做出假设):

  1. 堆栈存储器分配在一个连续的块中.
  2. 堆栈从高地址向低地址"向下"增长.
  3. 系统需要在分配的堆栈空间底部附近留出一些空间,以便优雅地处理堆栈外异常.我们不知道确切的保留空间,但我们会尝试保守地约束它.

通过这些假设,我们可以调整VirtualQuery以获取分配的堆栈的起始地址,并从一些堆栈分配的变量的地址中减去它(使用不安全的代码获得.)进一步减去我们对底部系统所需空间的估计.堆栈将给我们估计可用空间.

下面的代码通过调用递归函数并写出剩余的估计堆栈空间(以字节为单位)来演示这一点:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace ConsoleApplication1 {
    class Program {
        private struct MEMORY_BASIC_INFORMATION {
            public uint BaseAddress;
            public uint AllocationBase;
            public uint AllocationProtect;
            public uint RegionSize;
            public uint State;
            public uint Protect;
            public uint Type;
        }

        private const uint STACK_RESERVED_SPACE = 4096 * 16;

        [DllImport("kernel32.dll")]
        private static extern int VirtualQuery(
            IntPtr                          lpAddress,
            ref MEMORY_BASIC_INFORMATION    lpBuffer,
            int                             dwLength);


        private unsafe static uint EstimatedRemainingStackBytes() {
            MEMORY_BASIC_INFORMATION    stackInfo   = new MEMORY_BASIC_INFORMATION();
            IntPtr                      currentAddr = new IntPtr((uint) &stackInfo - 4096);

            VirtualQuery(currentAddr, ref stackInfo, sizeof(MEMORY_BASIC_INFORMATION));
            return (uint) currentAddr.ToInt64() - stackInfo.AllocationBase - STACK_RESERVED_SPACE;
        }

        static void SampleRecursiveMethod(int remainingIterations) {
            if (remainingIterations <= 0) { return; }

            Console.WriteLine(EstimatedRemainingStackBytes());

            SampleRecursiveMethod(remainingIterations - 1);
        }

        static void Main(string[] args) {
            SampleRecursiveMethod(100);
            Console.ReadLine();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里是前10行输出(intel x64,.NET 4.0,debug).鉴于1MB的默认堆栈大小,计数似乎是合理的.

969332
969256
969180
969104
969028
968952
968876
968800
968724
968648
Run Code Online (Sandbox Code Playgroud)

为简洁起见,上面的代码假定页面大小为4K.虽然这适用于x86和x64,但对于其他受支持的CLR架构可能不正确.您可以将其转换为GetSystemInfo以获取计算机的页面大小(SYSTEM_INFO结构的dwPageSize ).

请注意,这种技术不是特别便携,也不是未来的证据.pinvoke的使用限制了此方法对Windows主机的实用性.关于CLR堆栈的连续性和增长方向的假设可能适用于当前的Microsoft实现.但是,我(可能有限)读取CLI标准(公共语言基础结构,PDF,长读)似乎并不需要那么多的线程堆栈.就CLI而言,每个方法调用都需要一个堆栈帧; 但是,如果堆栈向上增长,如果局部变量堆栈与返回值堆栈分开,或者堆栈帧是否在堆上分配,那么它可能并不在意.

  • 如果有人要求一个常数,“程序可以安全使用多少堆栈”,我会同意“IYHTA,YCAI”哲学。另一方面,如果编写类似解析器的东西,可以使用递归来处理输入上任何预期级别的嵌套结构,则递归检查剩余堆栈空间并调用抛出“嵌套太深”似乎会更干净如果它不充分,则例外,而不是对嵌套施加一些任意限制。 (2认同)
  • 此检查在调试中可能也很有用,可以在您向堆栈溢出运行的情况下设置断点.断点将允许您转到调用堆栈的开头并检查每个变量.一旦抛出StackOverflowException,Visual Studio就不能再读取变量,为时已晚. (2认同)

Mat*_*vis 5

我正在添加此答案以供将来参考。:-)

Oren 的回答回答了 SO 的问题(如评论所提炼的那样),但它并未表明实际为堆栈分配了多少内存。要获得该答案,您可以在此处使用 Michael Ganß 的答案,我已在下面使用一些较新的 C# 语法对其进行了更新。

public static class Extensions
{
    public static void StartAndJoin(this Thread thread, string header)
    {
        thread.Start(header);
        thread.Join();
    }
}

class Program
{
    [DllImport("kernel32.dll")]
    static extern void GetCurrentThreadStackLimits(out uint lowLimit, out uint highLimit);

    static void WriteAllocatedStackSize(object header)
    {
        GetCurrentThreadStackLimits(out var low, out var high);
        Console.WriteLine($"{header,-19}:  {((high - low) / 1024),4} KB");
    }

    static void Main(string[] args)
    {
        WriteAllocatedStackSize("Main    Stack Size");

        new Thread(WriteAllocatedStackSize, 1024 *    0).StartAndJoin("Default Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 *  128).StartAndJoin(" 128 KB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 *  256).StartAndJoin(" 256 KB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 *  512).StartAndJoin(" 512 KB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 * 1024).StartAndJoin("   1 MB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 * 2048).StartAndJoin("   2 MB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 * 4096).StartAndJoin("   4 MB Stack Size");
        new Thread(WriteAllocatedStackSize, 1024 * 8192).StartAndJoin("   8 MB Stack Size");
    }
}
Run Code Online (Sandbox Code Playgroud)

有趣的是(以及我发布此内容的原因)是使用不同配置运行时的输出。作为参考,我在使用 .NET Framework 4.7.2(如果重要)的 Windows 10 Enterprise(Build 1709)64 位操作系统上运行它。

发布|任何 CPU(选中的首选 32 位选项):

发布|任何 CPU(首选 32 位选项未选中):

版本|x86:

Main    Stack Size :  1024 KB
Default Stack Size :  1024 KB // default stack size =   1 MB
 128 KB Stack Size :   256 KB // minimum stack size = 256 KB
 256 KB Stack Size :   256 KB
 512 KB Stack Size :   512 KB
   1 MB Stack Size :  1024 KB
   2 MB Stack Size :  2048 KB
   4 MB Stack Size :  4096 KB
   8 MB Stack Size :  8192 KB
Run Code Online (Sandbox Code Playgroud)

版本|x64:

Main    Stack Size :  4096 KB
Default Stack Size :  4096 KB // default stack size =   4 MB
 128 KB Stack Size :   256 KB // minimum stack size = 256 KB
 256 KB Stack Size :   256 KB
 512 KB Stack Size :   512 KB
   1 MB Stack Size :  1024 KB
   2 MB Stack Size :  2048 KB
   4 MB Stack Size :  4096 KB
   8 MB Stack Size :  8192 KB
Run Code Online (Sandbox Code Playgroud)

鉴于这些结果与文档一致,因此没有什么特别令人震惊的。然而,有点令人惊讶的是,在 Release|Any CPU 配置中运行时,默认堆栈大小为1 MB,而 Prefer 32-bit 选项未选中,这意味着它在 64 位操作系统上作为 64 位进程运行. 在这种情况下,我会假设默认堆栈大小为4 MB,就像 Release|x64 配置一样。

无论如何,我希望这对像我一样想要了解 .NET 线程的堆栈大小的人有用。

  • 对于“&lt;TargetFramework&gt;net5.0&lt;/TargetFramework&gt;”(以及早期版本的 .NET Core),main 的输出为“主堆栈大小:1536 KB”。因此.NET Core 的堆栈大小增加了 50%。但是,当我将配置更改为 Release|x64 时,该输出不会更改,这是意外的。我使用 Visual Studio 中的配置管理器进行了实验。 (2认同)