Resposta ao Desafio da Semana #10 [Hang/Crash com Loops em C# e VB.NET]
Por: Roberto Alexis Farah
Olá pessoal!
Eis a resposta ao desafio https://blogs.technet.com/latam/archive/2006/08/25/451782.aspx publicado semana passada.
SINTOMA
A execução do código vai ocasionar um crash de aplicação devido a estouro de pilha (StackOverflow).
Se você pensou em loop infinito infelizmente errou pois o loop será transitório até que a pilha estoure.
PROBLEMA
O código faz uma chamada a DoSomething() recursivamente, o que ocasiona o estouro de pilha (stack overflow).
SOLUÇÃO
Uma solução simples, que exige poucas mudanças seria…
Ao invés de:
public void DoSomething()
{
DoSomething();
_b1 = _a1;
}
Use:
public class B : A
{
public int _b1;
public void DoSomething()
{
base.DoSomething(); // Chamada a classe pai.
_b1 = _a1;
}
}
Desse modo o método “pai” é chamado do método “filho”, como indicado no comentário.
Agora, eis a demonstração do que ocorre ao se executar o código acima… Vamos aos porques…
Sempre que uma rotina é chamada é colocada na pilha de execução algumas informações relacionadas a chamada do método que após a execução são removidas.
Entretanto, em uma chamada recursiva a remoção do conteúdo da pilha ocorrerá apenas quando a recursão terminar!
No nosso cenário a recursão nunca vai acabar portanto, a pilha vai encher e ocorrerá uma exceção quando o limite for atingido.
Eis uma amostra da pilha, pois o número de frames é muito maior que isso:
OS Thread Id: 0xe8c (0)
ESP EIP
00033000 00ca0134 Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033008 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033010 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033018 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033020 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033028 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033030 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033038 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033040 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033048 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033050 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033058 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033060 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033068 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033070 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033078 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033080 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033088 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033090 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
00033098 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330a0 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330a8 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330b0 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330b8 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330c0 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330c8 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330d0 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330d8 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
000330e0 00ca013a Test.Program+B.DoSomething()
PARAMETERS:
this = 0x01271be0
Notem como ESP constantemente muda para refletir o incremento na pilha.
O endereço vai do maior número (endereço de memória virtual) para o menor, que é como a pilha cresce.
A pilha, por default, tem aproximadamente 1 mb de tamanho para cada thread, logo, ao se tentar passar esse limite ocorre a exceção.
Para nossa thread em questão temos:
TEB at 7ffdf000
ExceptionList: 0012f4ac
StackBase: 00130000
StackLimit: 00031000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 7ffdf000
EnvironmentPointer: 00000000
ClientId: 000013e8 . 00000e8c
RpcHandle: 00000000
Tls Storage: 00000000
PEB Address: 7ffd8000
LastErrorValue: 2
LastStatusValue: c000000f
Count Owned Locks: 0
HardErrorsMode: 0
A diferença da base e limite é: 0n1044480 (decimal) ou 0x000ff000 (hexadecimal)
É possível ver que o limite foi atingido. De fato, temos a exceção:
ExceptionAddress: 00ca0134 (Test!Test.Program+B.DoSomething()+0x00000014)
ExceptionCode: c00000fd (Stack overflow)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00032ffc
E a parte do código que ocasionou a exceção:
Test!Test.Program+B.DoSomething()+14 [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00ca0134 ff1568319100 call dword ptr ds:[913168h]
23: public int _b1;
24:
25: public void DoSomething()
26: {
> 27: DoSomething();
28: _b1 = _a1;
29: }
30: }
31:
32: static void Main(string[] args)
Eis as últimas chamadas na pilha até o momento do overflow. Aqui fica bem claro o preenchimento da pilha através das chamadas recursivas e finalizando em 00033000:
00032ff4 00000000
00032ff8 00000000
00032ffc 00000000
00033000 01271be0
00033004 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033008 01271be0
0003300c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033010 01271be0
00033014 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033018 01271be0
0003301c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033020 01271be0
00033024 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033028 01271be0
0003302c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033030 01271be0
00033034 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033038 01271be0
0003303c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033040 01271be0
00033044 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033048 01271be0
0003304c 00ca013a Test!Test.Program+B.DoSomething()+0x1a [C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\Program.cs @ 27]
00033050 01271be0
O que é o endereço 01271be0 que aparece acima?
Name: Test.Program+B
MethodTable: 00913130
EEClass: 00911364
Size: 20(0x14) bytes
GC Generation: 0
(C:\Development\My Tools\BLOG Articles\Article #15\Test\Test\bin\Debug\Test.exe)
Fields:
MT Field Offset Type VT Attr Value Name
790fed1c 4000001 4 System.Int32 0 instance 0 _a1
790fed1c 4000002 8 System.Int32 0 instance 0 _b1
790fed1c 4000003 c System.Int32 0 instance 0 _b1
Portanto, a cada nova chamada do método informações são colocadas na pilha e nunca removidas. Após várias execuções, quando a pilha atinge seu limite de ~1mb temos a exceção por estouro de pilha!
Para aqueles que estiverem se perguntando quando temos um sintoma de loop infinito, eis um exemplo:
while(1)
{
Console.WriteLine("X"); // Loop infinito sempre é acompanhado de alto consumo de CPU???
}
Por que nesse caso temos um loop infinito (com alto consumo de CPU) ao invés de um estouro de pilha?
A resposta é simples, nesse caso as informações colocadas na pilha quando a rotina dentro do loop é chamada são removidas quando a execução retorna para o loop, ou seja, o espaço necessário na pilha para permitir a chamada desse método é muito pequeno pois é constantemente limpa quando a execução de Console.WriteLine() finaliza. Não há uma serialização de chamadas na pilha que nunca são retornadas como no caso do desafio colocado.
Agora atente para um detalhe importante: No loop acima temos um cenário de alto consumo de CPU não por ser um loop infinito, mas por ser um loop sem uma pausa para o processador “respirar”, digo, sem por exemplo, uma chamada para Sleep(200). O que quero dizer com isso é que podem haver cenários de loops infinitos de alta CPU ou loops infinitos de baixa CPU, tudo depende de haver chamadas dentro do loop que possibilitem a CPU dedicar mais tempo de processamento para as outras threads ou não.
Acredito que após entender esse desafio você estará apto a examinar um código suspeito e identificar se é candidato a crash por stack overflow ou um loop infinito (hang, aplicação pendurada ) , e se poderia haver um consumo elevado de CPU ou não. J
Como havia dito ao postar o desafio, algumas semanas atrás usei-o em entrevistas e curiosamente ninguém acertou 100%.
O loop infinito foi a resposta mais usada.
Até o próximo desafio.