写了多年代码,你会 StackOverflow 吗
写了多年代码,你会 StackOverflow 吗
Intro
准备写一个傻逼代码的系列文章,怎么写 StackOverflow
的代码,怎么写死锁代码,怎么写一个把 CPU 跑满,怎么写一个 OutOfMemory
的代码。
今天主要来看 StackOverflow
,StackOverlow
是一个著名的问答社区,做开发相关的应该都会知道这个网站,很多你遇到的问题都可以在这个网站上找到答案。
但这不是我的主题,今天的主题是如何写一段代码导致 StackOverflow
如何让你的代码发生 StackOverflow
在 C# 中有一个 StackOverflowException
的异常,发生 StackOverlow
也就是触发 StackOverlowException
,关于 StackOverflowException
的介绍可以参考微软文档
StackOverflowException
is thrown for execution stack overflow errors, typically in case of a very deep or unbounded recursion.
这里节选了一小段话, 在执行堆栈发生溢出错误的时候会抛出 StackOverflowException
,典型的案例就是一个特别深的或者没有边界的递归。
这里说明了一种典型的会发生 StackOverflowException
的示例就是递归,这种方式可能大家都知道了。
除此之外还有一种情况也会引发 StackOverflowException
,微软的文档上没有明确说明,我们知道,在 C# 中,值类型的内存分配一般是在栈上的,这个栈也就是线程栈,一个线程的栈是有限制的,这里可以出一个面试题,线程栈有多大
线程栈默认 1M,也是最大值,最大也是 1M(下次面试的时候可别不知道了哈)
详细介绍可以看文档 https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.-ctor?view=netcore-3.1
在线程栈上分配内存不够的时候同样也会引发 StackOverflowException
所以写出 StackOverflow
代码的两种方式就有了
- 死递归(没有边界,一直在循环调用自己)
- 线程栈内存分配
死递归
死递归这种方式最简单了,写一个方法调用自己即可,来看一个示例:
public static void Test1()
{
Test1();
}
通常这样的代码,大家一眼就能看出来,一般也不会写这样的代码,通常写出 StackOverflow
的代码都是有参数的,参数传递有问题所以导致的 StackOverflow
,比如下面这样的一个示例:
// StackOverflow
public static void Test2(int num)
{
if(num > 0)
{
Test2(num);
}
}
// work
public static void Test3(int num)
{
if(num > 0)
{
Test3(--num);
}
}
下面的代码是正常代码,上面的代码会 StackOverlow
,只是因为一行代码的失误导致的问题,这种情况更为常见一些
我之前也写过一个
StackOverflow
的代码,之前的代码也是带参数的递归,递归的参数是一个枚举,方法体里有多个枚举参数,结果在方法体递归的时候传错了参数,导致了一个死递归
线程栈内存分配
通常值类型的分配是在线程栈上的,但是我们也不会在一个线程上分配很多的值类型对象,所以通常也不会因为值类型的内存分配而导致 StackOverflowException
在 C# 7.2 之后引入了 Span
(.net core 2.1 之后默认包含了 Span
,.netstandard2.1
也包含了 Span
,如果是低版本或者 .net framework 需要额外引用一个包来支持 Span),我们可以在安全代码中使用 stackalloc
来实现在线程栈上分配内存,我们要使用这个来做测试,来看下面的测试代码:
public static void Test()
{
ReadOnlySpan<byte> bytes = stackalloc byte[1024 * 1024];
Console.WriteLine($"{bytes.Length} passed");
bytes = stackalloc byte[1024 * 1024 + 1];
Console.WriteLine($"{bytes.Length} passed");
}
可以看到在超过 1M 的时候就会发生 StackOverflow
Thread MaxStackSize
如果你比较细心,使用 Thread
比较多的话,你会发现 Thread
在实例化的时候,可以在构造方法里指定一个参数来指定线程的最大栈,参考文档:https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.-ctor?view=netcore-3.1
public Thread (System.Threading.ThreadStart start, int maxStackSize)
所以我们也可以指定一个比较小的 maxStackSize
来更容易测试出来 StackOverflow
来看一个示例:
public static void Test2()
{
var thread = new Thread(() =>
{
ReadOnlySpan<byte> bytes = stackalloc byte[16*1024+1];
Console.WriteLine($"{bytes.Length} passed");
bytes = stackalloc byte[32*1024+1];
Console.WriteLine($"{bytes.Length} passed");
bytes = stackalloc byte[256*1024+1];
Console.WriteLine($"{bytes.Length} passed");
}, 1);
thread.IsBackground = true;
thread.Start();
}
这里可以看到,我直接指定了 maxStackSize
为 1,但是实际测试下来并不是1,按微软的文档上所说,最小值是 256kb(如果我没理解错的话),但是实际测试下来并不是,没有到 256kb 就发生了 StackOverflow
(有大神看到还望指导一下),上面的测试输出结果
再来看一个递归的示例:
public static void Test3()
{
var thread = new Thread(() =>
{
TestMethod(1024);
}, 1);
thread.IsBackground = true;
thread.Start();
}
private static void TestMethod(int num)
{
if(num > 0)
{
num--;
TestMethod(num);
}
}
输出结果如下:
上面使用的数字也许不会在你的电脑上发生异常,你可以尝试自己调试一个临界值,或者直接设置一个比较大的值
More
StackOverflowException
不能通过 try...catch 来捕捉,当发生 StackOverflowException
时应用程序就会直接退出,通常线程栈上分配内存不够的情况基本上是不会发生的,所以对于怎么避免 StackOverflowException
我们只需要在写递归代码的时候小心一些,不要写成死循环递归就可以了。
对于在线程栈上分配内存,也可以通过写非安全代码来实现,有兴趣的童鞋可以自己研究一下。
Reference
- https://docs.microsoft.com/en-us/dotnet/api/system.stackoverflowexception?view=netcore-3.1
- https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.-ctor?view=netcore-3.1
- https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc
- https://github.com/WeihanLi/SamplesInPractice/blob/master/StupidSamples/StackOverflowSample.cs