성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ArrayPool<T> 소개</h1> <p> 이미 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1'>ArrayPool<T></a>에 대해 다음과 같은 훌륭한 글이 있지만, ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Pooling large arrays with ArrayPool ; <a target='tab' href='https://adamsitnik.com/Array-Pool/'>https://adamsitnik.com/Array-Pool/</a> </pre> <br /> 그래도 대충 정리를 해보겠습니다. <br /> <br /> <hr style='width: 50%' /><br /> <br /> 우선 기본적인 사용법은 Pool에서 배열을 받아오고/반환하는 절차로 이뤄집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // .NET Core byte[] buffer = ArrayPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Rent(1024)</span>; Console.WriteLine($"buffer[0] == {buffer[0]}"); // buffer[0] == 0 ArrayPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Return(buffer)</span>; </pre> <br /> 주의해야 할 점은, 기본적으로는 반환한 버퍼가 그대로 재사용되므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); // buffer[0] == 0 <span style='color: blue; font-weight: bold'>buffer[0] = 5;</span> ArrayPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Return</span>(buffer); buffer = ArrayPool<byte>.Shared.<span style='color: blue; font-weight: bold'>Rent</span>(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); <span style='color: blue; font-weight: bold'>// buffer[0] == 5</span> </pre> <br /> 이전 데이터가 남아 있어 일반적인 new 할당과는 달리 0 초기화를 기대해서는 안 됩니다. 아니면, 반환할 때 명시적으로 초기화를 시키는 옵션을 줘야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); // buffer[0] == 0 <span style='color: blue; font-weight: bold'>buffer[0] = 5;</span> ArrayPool<byte>.Shared.Return(buffer, <span style='color: blue; font-weight: bold'>/* clearArray: */ true</span>); buffer = ArrayPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); <span style='color: blue; font-weight: bold'>// buffer[0] == 0</span> </pre> <br /> 위의 상황을 좀 더 확대 해석해 보면, Rent로 얻은 버퍼를 Return 후에 사용하지 않도록 주의해야 합니다. Pool이라는 성격상 참조 그대로 살아 있고 재사용하는 유형이기 때문에 그런 실수를 하게 된다면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); // buffer[0] == 0 ArrayPool<byte>.Shared.Return(buffer, <span style='color: blue; font-weight: bold'>/* clearArray: */ true</span>); <span style='color: blue; font-weight: bold'>buffer[0] = 5;</span> /* 혹은 buffer를 향후 지속되는 개체에 전달했다거나 */ buffer = ArrayPool<byte>.Shared.Rent(1024); Console.WriteLine($"buffer[0] == {buffer[0]}"); <span style='color: blue; font-weight: bold'>// buffer[0] == 5</span> </pre> <br /> 런타임 시에 원인을 추적하기 힘든 오류로 발전할 여지가 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 요구 크기에 대한 구획을 나누기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // System.Buffers.Utilities.SelectBucketIndex internal static int SelectBucketIndex(int bufferSize) { uint value = (uint)(bufferSize - 1) >> 4; return 32 - BitOperations.LeadingZeroCount(value); } </pre> <br /> (내부 구현이므로 향후 바뀔 수 있지만) 512 바이트 구간에 대해서는 같은 버퍼를 반환하므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] buffer = ArrayPool<byte>.Shared.Rent(<span style='color: blue; font-weight: bold'>1000</span>); Console.WriteLine(<span style='color: blue; font-weight: bold'>buffer.Length</span>); // 출력 결과 <span style='color: blue; font-weight: bold'>1024</span> buffer = ArrayPool<byte>.Shared.Rent(<span style='color: blue; font-weight: bold'>513</span>); Console.WriteLine(<span style='color: blue; font-weight: bold'>buffer.Length</span>); // 출력 결과 <span style='color: blue; font-weight: bold'>1024</span> buffer = ArrayPool<byte>.Shared.Rent(<span style='color: blue; font-weight: bold'>512</span>); Console.WriteLine(<span style='color: blue; font-weight: bold'>buffer.Length</span>); // 출력 결과 <span style='color: blue; font-weight: bold'>512</span> </pre> <br /> Rent 메서드로 요청한 크기에 정확히 일치하는 버퍼가 반환된다고 가정해서는 안 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 기본 CLR 스레드 풀을 사용하지 않고 별도로 정의할 수 있는 요구가 있는 것처럼,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 분리된 ThreadPool 사용 - Smart Thread Pool ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/986'>https://www.sysnet.pe.kr/2/0/986</a> </pre> <br /> ArrayPool도 그럴 수 있는데요, 다행히 이것은 해당 타입 내에서 기능을 제공하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>// Shared가 아닌, 새로운 ArrayPool을 생성</span> <span style='color: blue; font-weight: bold'>ArrayPool<byte> newPool = ArrayPool<byte>.Create();</span> byte [] buffer = <span style='color: blue; font-weight: bold'>newPool.Rent</span>(1000); <span style='color: blue; font-weight: bold'>newPool.Return</span>(buffer); </pre> <br /> 재미있는 점은, Shared의 Pool 관리를 담당하는 타입과 Create의 Pool 관리를 담당하는 타입이 다르다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static ArrayPool() { ArrayPool<T>.s_shared = new <span style='color: blue; font-weight: bold'>TlsOverPerCoreLockedStacksArrayPool</span><T>(); } public static ArrayPool<T> Create() { return new <span style='color: blue; font-weight: bold'>ConfigurableArrayPool</span><T>(); } </pre> <br /> 이름에서 유추할 수 있지만 Shared의 경우 TLS 성격을 갖기 때문에 Shared로 접근하는 스레드 별로 관리 개체가 생성되므로 Rent/Return 호출 시에 별도의 lock이 필요 없습니다. 반면 ConfigurableArrayPool의 경우 단독 개체가 생성되는 것이고 thread-safe을 보장하기 위해 Rent/Return 내부에서 lock이 사용되므로 약간의 성능 손실이 발생합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> "<a target='tab' href='https://adamsitnik.com/Array-Pool/'>Pooling large arrays with ArrayPool</a>" 글에 보면, 마지막 즈음에 Pool 관련한 ETW Event Provider를 소개하고 있습니다. 그렇다면, 지난 글의 in-proc 모니터링을,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - (.NET Core 2.2부터 가능한) 프로세스 내부에서 CLR ETW 이벤트 수신 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12474'>https://www.sysnet.pe.kr/2/0/12474</a> </pre> <a name='etw_src'></a> <br /> 다음과 같이 간단하게 접목해 볼 수 있습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // .NET Core 2.2 using System; using System.Buffers; using System.Diagnostics.Tracing; using System.Threading; namespace ConsoleApp2 { class Program { static MyEventListener listener = new MyEventListener(); static void Main(string[] args) { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}"); byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); } } } internal class MyEventListener : EventListener { protected override void OnEventSourceCreated(EventSource eventSource) { base.OnEventSourceCreated(eventSource); <span style='color: blue; font-weight: bold'>if (eventSource.Name == "System.Buffers.ArrayPoolEventSource") { EnableEvents(eventSource, EventLevel.Informational); }</span> } protected override void OnEventWritten(EventWrittenEventArgs eventData) { int tid = Thread.CurrentThread.ManagedThreadId; if (eventData.EventName == "BufferAllocated") { Console.WriteLine($"{tid} {eventData.EventName}"); } } } /* 출력 결과 1 1 BufferAllocated */ </pre> <br /> (결과를 보면, Main 메서드를 실행하는 스레드와 OnEventWritten 메서드를 실행되는 스레드가 동일하다는 것에서 실시간 호출임을 짐작게 합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, Rent 후 Return을 하지 않으면 어떻게 될까요?<br /> <br /> ArrayPool의 내부 구현이 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.weakreference'>WeakReference</a> 같은 타입을 사용한 Cache 형식이 아닌, 단순히 일정 수의 버퍼를 할당해 보관해 놓는 것이므로 Return을 하지 않으면 쌓이게 되어 있습니다.<br /> <br /> 따라서, 기본 구현에 따라,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public override T[] Rent(int minimumLength) { // ...[생략]... int num = Utilities.SelectBucketIndex(minimumLength); T[] array; if (num < this._buckets.Length) { int num2 = num; while (true) { array = this._buckets[num2].Rent(); if (array != null) { break; } <span style='color: blue; font-weight: bold'>if (++num2 >= this._buckets.Length || num2 == num + 2) { goto IL_86; }</span> } // ...[생략]... return array; IL_86: <span style='color: blue; font-weight: bold'>array = new T[this._buckets[num]._bufferLength];</span> } else { <span style='color: blue; font-weight: bold'>array = new T[minimumLength];</span> } // ...[생략]... return array; } </pre> <br /> Rent를 원하는 크기의 Bucket에 여유가 없으면 한 단계 큰 Bucket에서 다시 여유가 있는지 확인하고, 그래도 없으면 Pool이 관리하지 않는 새로운 버퍼를 할당해 반환해 버립니다. 따라서 일반적으로 우리가 알고 있는 DB 연결 풀이나 스레드 풀처럼 Free 자원이 고갈되었을 때 대기를 하는 것과는 달리 (어느 정도 Pool의 bucket 크기에 따라 leak이 발생하지만) 전체적으로 동작하는 데에는 영향을 주지 않습니다.<br /> <br /> 실제로 ETW 이벤트를 활용해 이런 상황을 테스트해 볼까요? ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Buffers; using System.Diagnostics.Tracing; using System.Threading; namespace ConsoleApp2 { class Program { static MyEventListener listener = new MyEventListener(); static ArrayPool<byte> _q = ArrayPool<byte>.Create(8192 * 4, <span style='color: blue; font-weight: bold'>5</span>); // 구간 별 최대 크기 5 static void Main(string[] args) { Console.WriteLine($"Main TID: {Thread.CurrentThread.ManagedThreadId}"); for (int i = 0; <span style='color: blue; font-weight: bold'>i < 12</span>; i++) { byte[] buffer = <span style='color: blue; font-weight: bold'>_q.Rent(4000);</span> Console.WriteLine($", BufferLen: {buffer.Length}"); } } } } internal class MyEventListener : EventListener { protected override void OnEventSourceCreated(EventSource eventSource) { Console.WriteLine(eventSource); base.OnEventSourceCreated(eventSource); if (eventSource.Name == "System.Buffers.ArrayPoolEventSource") { EnableEvents(eventSource, EventLevel.Informational); } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { int tid = Thread.CurrentThread.ManagedThreadId; if (eventData.EventName == "BufferAllocated") { if (eventData.PayloadNames.Contains("reason") == true) { BufferAllocatedReason reason = (BufferAllocatedReason)eventData.Payload[4]; Console.Write($"OnEventWritten TID: {tid}, {eventData.EventName}, {reason}"); } } } internal enum BufferAllocatedReason { <span style='color: blue; font-weight: bold'>Pooled,</span> OverMaximumSize, <span style='color: blue; font-weight: bold'>PoolExhausted</span> } } </pre> <br /> (쉬운 테스트를 위해) 크기 별 최대 보관 수가 5인 ArrayPool을 만들었고, 4000 바이트의 버퍼를 12번 요구한 출력 결과는 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 8192 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 8192 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 8192 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 8192 OnEventWritten TID: 1, BufferAllocated, Pooled, BufferLen: 8192 OnEventWritten TID: 1, BufferAllocated, <span style='color: blue; font-weight: bold'>PoolExhausted</span>, BufferLen: 4096 OnEventWritten TID: 1, BufferAllocated, PoolExhausted, BufferLen: 4096 </pre> <br /> 보다시피, 4096 바이트로 5번, 8192 바이트로 5번을 반환받은 후부터는 PoolExhausted로 나오고 있습니다. (세부적인 규칙은 바뀔 수 있으므로 이에 가정한 코딩을 해서는 안 됩니다.)<br /> <br /> (PoolExhausted 상태에서도 4000 바이트 요구에 대해 굳이 Pool의 규격에 맞게 4096 바이트를 할당해 반환한다는 점도 재미있습니다. ^^)<br /> <br /> <hr style='width: 50%' /><br /> <br /> .NET Framework의 경우 ArrayPool 타입이 기본 BCL에 없으므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ArrayPool<T> Class ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1'>https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1</a> 최소 요구 사항: .NET 5.0, .NET Core 1.0, .NET Standard 2.1 </pre> <br /> Nuget에서 제공하는 별도 라이브러리를 참조해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.Buffers ; <a target='tab' href='https://www.nuget.org/packages/System.Buffers/'>https://www.nuget.org/packages/System.Buffers/</a> </pre> (.NET Framework 4.5부터 참조 가능합니다.)<br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package System.Buffers </pre> <br /> 이하 사용법은 .NET Core의 것과 동일합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1698&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
3393
(왼쪽의 숫자를 입력해야 합니다.)