성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
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# - async/await 그리고 스레드 (4) 비동기 I/O 재현</h1> <p> 해본 김에, 이제 비동기 I/O에서의 async/await도 구현해 보겠습니다. 그런데, 이것을 설명하기 위해 부가적으로 Windows의 "비동기 I/O"에 대한 기본적인 이해가 필요합니다. 그러니까... 결국 이번 주제를 위해 다음의 글이 쓰인 것입니다. ^^;<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13059'>https://www.sysnet.pe.kr/2/0/13059</a> </pre> <br /> 그리고, 사실 위의 글만 이해했다면 async/await과의 연동은 이미 설명한 것이나 다름없습니다. 왜냐하면 결국 비동기 I/O에 대한 async/await은 위의 글에서 만든 MyAsyncFileStream에 "<a target='tab' href='https://www.sysnet.pe.kr/2/0/13055'>C# - async/await 그리고 스레드 (1)</a>" 글에서 만든 MyTask를 연결만 하면 되기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 예를 한번 들어볼까요? ^^ 우리는 그동안 C#에서 다음과 같은 식으로 간단하게 파일 I/O를 비동기로 접근할 수 있었습니다.<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; internal class Program { static async Task Main(string[] args) { Console.WriteLine($"[{DateTime.Now}] 1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); string filePath = @"test.txt"; FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, 0, <a target='tab' href='https://www.sysnet.pe.kr/2/0/12250'>true</a>); byte[] buffer = <span style='color: blue; font-weight: bold'>new byte[2];</span> int readBytes = <span style='color: blue; font-weight: bold'>await fs.ReadAsync(buffer, 0, buffer.Length);</span> Console.WriteLine($"[{DateTime.Now}] 2단계: {BitConverter.ToString(buffer)} threadid == {Thread.CurrentThread.ManagedThreadId}"); readBytes = <span style='color: blue; font-weight: bold'>await fs.ReadAsync(buffer, 0, buffer.Length);</span> Console.WriteLine($"[{DateTime.Now}] 3단계: {BitConverter.ToString(buffer)} threadid == {Thread.CurrentThread.ManagedThreadId}"); } } /* 출력 결과 */ /* <span style='color: blue; font-weight: bold'>test.txt의 파일 내용: 1234567890</span> [2022-05-15 오전 9:01:43] 1단계: threadid == <span style='color: blue; font-weight: bold'>1</span> [2022-05-15 오전 9:01:43] 2단계: <span style='color: blue; font-weight: bold'>31-32</span> threadid == <span style='color: blue; font-weight: bold'>4</span> [2022-05-15 오전 9:01:43] 3단계: <span style='color: blue; font-weight: bold'>33-34</span> threadid == <span style='color: blue; font-weight: bold'>4</span> */ </pre> <br /> 자, 이 코드와 유사한 것을 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13061#myasyncfilestream'>지난 글에서 만든 MyAsyncFileStream</a>에 구현해 보겠습니다. 추가해야 할 코드는 간단합니다. MyTask 코드는 더 추가할 것이 없고, 단지 MyAsyncFileStream에 MyTask를 반환하는 ReadAsync/WriteAsync만 아래와 같이 추가해 주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.Win32.SafeHandles; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Console1 { public class MyAsyncFileStream : IDisposable { long _pos; SafeFileHandle? _pHandle; public MyAsyncFileStream(string filePath, FileAccess fileAccess, FileShare fileShare, FileMode fileMode) { _pHandle = NativeMethods.CreateFile(filePath, fileAccess, fileShare, IntPtr.Zero, fileMode, <span style='color: blue; font-weight: bold'>NativeMethods.FILE_FLAG_OVERLAPPED</span>, IntPtr.Zero); if (_pHandle.IsInvalid == true) { int lastError = Marshal.GetHRForLastWin32Error(); Marshal.ThrowExceptionForHR(lastError); } } public long Position { get { return _pos; } set { _pos = value; SeekCore(_pos, SeekOrigin.Begin); } } void SeekCore(long offset, SeekOrigin origin) { if (NativeMethods.SetFilePointerEx(_pHandle!, offset, out long moved, (uint)origin) == true) { _pos = moved; } } public unsafe <span style='color: blue; font-weight: bold'>MyTask WriteAsync</span>(byte[] buffer) { uint written; NativeOverlapped o = new NativeOverlapped(); o.OffsetLow = unchecked((int)_pos); o.OffsetHigh = (int)(_pos >> 32); SeekCore(buffer.Length, SeekOrigin.Current); EventWaitHandle writeEvent = new EventWaitHandle(false, EventResetMode.AutoReset); o.EventHandle = writeEvent.SafeWaitHandle.DangerousGetHandle(); int ret = NativeMethods.WriteFile(_pHandle!, buffer, buffer.Length, out written, ref o); <span style='color: blue; font-weight: bold'>MyTask task = new MyTask();</span> if (ret == 1) { task.SetComplete(); } else { int lastError = Marshal.GetLastWin32Error(); if (lastError == 0) { task.SetComplete(); } else if (lastError == 997) // ERROR_IO_PENDING == 997 { OverlappedParameter op = new OverlappedParameter() { Event = writeEvent, <span style='color: blue; font-weight: bold'>Task = task,</span> }; <span style='color: blue; font-weight: bold'>op.WaitHandle = ThreadPool.RegisterWaitForSingleObject(writeEvent, ioCompletedCallback, op, -1, true); op.TryUnregister();</span> } else { throw Marshal.GetExceptionForHR(NativeMethods.HRESULT_FROM_WIN32(lastError)) ?? new IOException(); } } return task; } public unsafe <span style='color: blue; font-weight: bold'>MyTask ReadAsync</span>(byte[] buffer) { NativeOverlapped no = new NativeOverlapped(); no.OffsetLow = unchecked((int)_pos); no.OffsetHigh = (int)(_pos >> 32); SeekCore(buffer.Length, SeekOrigin.Current); EventWaitHandle readEvent = new EventWaitHandle(false, EventResetMode.AutoReset); no.EventHandle = readEvent.SafeWaitHandle.DangerousGetHandle(); int ret = NativeMethods.ReadFile(_pHandle!, buffer, buffer.Length, out uint readLen, ref no); <span style='color: blue; font-weight: bold'>MyTask task = new MyTask();</span> if (ret == 1) { task.SetComplete(); } else { int lastError = Marshal.GetLastWin32Error(); if (lastError == 0) { task.SetComplete(); } else if (lastError == NativeMethods.ERROR_IO_PENDING) // 997 == Overlapped I/O operation is in progress. { OverlappedParameter op = new OverlappedParameter { Event = readEvent, <span style='color: blue; font-weight: bold'>Task = task,</span> }; <span style='color: blue; font-weight: bold'>op.WaitHandle = ThreadPool.RegisterWaitForSingleObject(readEvent, ioCompletedCallback, op, -1, true); op.TryUnregister();</span> } else { throw Marshal.GetExceptionForHR(NativeMethods.HRESULT_FROM_WIN32(lastError)) ?? new IOException(); } } return task; } private static void <span style='color: blue; font-weight: bold'>ioCompletedCallback</span>(object? state, bool timedOut) { OverlappedParameter? op = state as OverlappedParameter; if (op == null) { return; } op.Done(); <span style='color: blue; font-weight: bold'>MyTask? task = op.Task;</span> if (task == null) { return; } if (timedOut == true) { // task.SetException(); return; } <span style='color: blue; font-weight: bold'>task.SetComplete(); // 이 단계에서 await으로 나뉜 "분할 2" 코드를 수행</span> } public void Dispose() { if (_pHandle != null) { _pHandle.Dispose(); _pHandle = null; } } } public class OverlappedParameter { // ...[생략]... } } </pre> <br /> 끝입니다. ^^ 이제 우리도 FileStream이 했던 것처럼, MyAsyncFileStream과 MyTask의 협업으로 다음과 같이 (거의 유사하게) 코딩을 할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static async Task Main(string[] args) { Console.WriteLine($"[{DateTime.Now}] 1단계: threadid == {Thread.CurrentThread.ManagedThreadId}"); string filePath = @"test.txt"; MyAsyncFileStream fs = new MyAsyncFileStream(filePath, FileAccess.ReadWrite, FileShare.ReadWrite, FileMode.Open); { byte[] buffer = Encoding.UTF8.GetBytes("12"); <span style='color: blue; font-weight: bold'>await fs.WriteAsync(buffer);</span> buffer = Encoding.UTF8.GetBytes("34"); <span style='color: blue; font-weight: bold'>await fs.WriteAsync(buffer);</span> } fs.Position = 0; { byte[] buffer = new byte[2]; <span style='color: blue; font-weight: bold'>await fs.ReadAsync(buffer);</span> Console.WriteLine($"[{DateTime.Now}] 2단계: {BitConverter.ToString(buffer)} threadid == {Thread.CurrentThread.ManagedThreadId}"); <span style='color: blue; font-weight: bold'>await fs.ReadAsync(buffer);</span> Console.WriteLine($"[{DateTime.Now}] 3단계: {BitConverter.ToString(buffer)} threadid == {Thread.CurrentThread.ManagedThreadId}"); } } /* 출력 결과 */ /* test.txt의 파일 내용: 1234567890 [2022-05-15 오전 9:58:09] 1단계: threadid == 1 [2022-05-15 오전 9:58:09] 2단계: 31-32 threadid == 8 [2022-05-15 오전 9:58:09] 3단계: 33-34 threadid == 8 */ </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1935&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<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;' > [2022-05-15 오전 9:58:09] 1단계: threadid == 1 [2022-05-15 오전 9:58:09] 2단계: 31-32 threadid == 8 [2022-05-15 오전 9:58:09] 3단계: 33-34 threadid == 8 </pre> <br /> Main 메서드를 시작한 thread는 1번인데요, 이어서 ReadAsync를 호출해 비동기 I/O를 개시하고 RegisterWaitForSingleObject로 ioCompletedCallback을 등록합니다. 이후 I/O가 완료되면 스레드 풀의 여유 스레드(위의 출력에서는 8번 스레드)에 의해 ioCompletedCallback이 호출됩니다. 그리고 그 8번 스레드는 다시 두 번째 ReadAsync를 호출하고 스레드 풀에 돌아갑니다. 마찬가지로 다시 한번 ioCompletedCallback이 발생하고 그것을 (스레드 풀에 돌아갔던) 8번 스레드가 실행하는 결과를 보여줍니다.<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;' > Asynchronous disk I/O appears as synchronous on Windows ; <a target='tab' href='https://learn.microsoft.com/en-US/troubleshoot/windows/win32/asynchronous-disk-io-synchronous'>https://learn.microsoft.com/en-US/troubleshoot/windows/win32/asynchronous-disk-io-synchronous</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> Frequently, asynchronous I/O operations behave just as synchronous I/O.<br /> ...<br /> if an operation is completed immediately, then &NumberOfBytesRead passed into ReadFile is valid for the number of bytes read. In this case, ignore the OVERLAPPED structure passed into ReadFile; do not use it with GetOverlappedResult or WaitForSingleObject.<br /> </div><br /> <br /> ReadAsync는 ReadFile 호출에서 동기 방식으로 그순간 완료가 돼 반환하는 경우도 있으므로 이전 단계의 async callback을 실행하던 8번 스레드가 두 번째 async read를 끝까지 실행하는 경우도 가능하므로 해석은 상황에 따라 다를 수 있습니다.<br /> <br /> 그런데 실제로 이 상황이 있는지 확인하려고 ReadAsync (또는 WriteAsync)의 반환에서 확인 코드를 넣어봤지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > int ret = NativeMethods.ReadFile(_pHandle!, buffer, buffer.Length, out uint readLen, ref no); MyTask task = new MyTask(); if (ret == 1) { <span style='color: blue; font-weight: bold'>Console.WriteLine("Sync");</span> task.SetComplete(); } </pre> <br /> 아무리 실행해도 "Sync" 메시지가 화면에 출력되는 경우를 볼 수 없었습니다. (혹시, 이와 관련해서 저 메시지를 볼 수 있는 "요령"을 알고 계신 분은 덧글 부탁드립니다. ^^)<br /> <br /> 마지막으로, 위의 예제 코드를 실행하다 보면 간혹 다음과 같은 식으로 2단계와 3단계의 스레드가 다르게 나오는 경우를 볼 수 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [2022-05-15 오전 10:06:57] 1단계: threadid == 1 [2022-05-15 오전 10:06:57] 2단계: 31-32 threadid == 8 [2022-05-15 오전 10:06:57] 3단계: 33-34 threadid == 11 </pre> <br /> 8번 스레드가 스레드 풀에 여유 스레드 자원으로 안착하기도 전에 두 번째 ioCompletedCallback이 호출돼 11번 여유 스레드가 호출된 것으로 해석할 수 있습니다.<br /> <br /> 자, 그럼 이것으로 async/await에 관한 스레드 이야기는 모두 마쳤습니다. async/await의 마법 같은 동작 방식이, 아마도 지금까지의 글을 잘 읽고 이해하셨다면 더 이상 마법이 아닌, FileStream과 Task와 C# 컴파일러의 노력으로 빚은 <span style='text-decoration: line-through'>노가다</span> 예술임을 느끼실 수 있을 것입니다. ^^ <br /> <br /> <hr style='width: 50%' /><br /> <br /> 이 글에서 사용한 MyTask, MyAsyncFileStream은 async/await 학습을 위한 딱 그 정도 역할만 하도록 만들어진 것이므로 현업에서 쓰는 것에는 권장하지 않습니다. (미구현 코드가 너무 많기도 하고, 안정성을 보장할 수 없습니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1368
(왼쪽의 숫자를 입력해야 합니다.)