성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
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# - FILE_FLAG_OVERLAPPED가 적용된 파일의 읽기/쓰기 시 Position 관리</h1> <p> 아래의 글을,<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 /> 이렇게 실습해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Console1; using Microsoft.Win32.SafeHandles; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; [assembly: SupportedOSPlatform("windows")] class Program { static void Main(string[] args) { byte[] buffer = Encoding.UTF8.GetBytes("tt"); byte[] buffer2 = Encoding.UTF8.GetBytes("ee"); File.Delete(@"C:\temp\test.txt"); using (SafeFileHandle pHandle = NativeMethods.CreateFile(@"c:\temp\test.txt", (uint)FileAccess.ReadWrite, 0, IntPtr.Zero, (uint)2, NativeMethods.FILE_FLAG_OVERLAPPED, IntPtr.Zero)) { <span style='color: blue; font-weight: bold'>WriteAsync(pHandle, buffer); Thread.Sleep(1000); WriteAsync(pHandle, buffer2);</span> } Console.WriteLine("Press Enter to exit ..."); Console.ReadLine(); } private static void WriteAsync(SafeFileHandle pHandle, byte[] buf) { uint written; NativeOverlapped o = new NativeOverlapped(); EventWaitHandle writeEvent = new EventWaitHandle(false, EventResetMode.AutoReset); o.EventHandle = writeEvent.SafeWaitHandle.DangerousGetHandle(); if (NativeMethods.WriteFile(pHandle, buf, buf.Length, out written, ref o) == false) { int lastError = Marshal.GetLastWin32Error(); if (lastError == 997) // ERROR_IO_PENDING == 997 { OverlappedParameter op = new OverlappedParameter() { Event = writeEvent, }; op.WaitHandle = ThreadPool.RegisterWaitForSingleObject(writeEvent, WriteCompleted, op, -1, false); op.TryUnregister(); } else { // Write File Error Console.WriteLine("Write File Error"); writeEvent.Close(); } } } public static void WriteCompleted(object? objState, bool timedOut) { Console.WriteLine($"{DateTime.Now} async WriteCompleted"); OverlappedParameter? op = objState as OverlappedParameter; if (op == null) { return; } op.Done(); } } public class OverlappedParameter { // ...[생략]... } </pre> <br /> 2번의 WriteAsync 호출에서 전달한 2바이트 내용이,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > WriteAsync(pHandle, buf); // buf == 'tt' Thread.Sleep(1000); WriteAsync(pHandle, buf2); // buf2 == 'ee' </pre> <br /> "FileStream.Position == 0"인 위치에 써지기 때문에 출력 파일의 결과는 "ee"만 담고 있게 됩니다. 실제로 FILE_FLAG_OVERLAPPED가 적용된 파일은 Read/Write 동작을 한 후 파일 포인터를 전혀 변경하지 않습니다. 가령, 다음과 같이 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfilepointerex'>SetFilePointerEx</a> Win32 API를 호출해 현재의 파일 포인터 위치를 구해봐도,<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'>WriteAsync(pHandle, buf);</span> Thread.Sleep(1000); <span style='color: blue; font-weight: bold'>Console.WriteLine(GetCurrentPosition(pHandle)); // 출력 결과: 0</span> WriteAsync(pHandle, buf2); private static long GetCurrentPosition(SafeFileHandle handle) { if (NativeMethods.SetFilePointerEx(handle, 0, out long result, NativeMethods.FILE_CURRENT) == true) { return result; } return 0; } </pre> <br /> 0이 나옵니다. 좀 이상한가요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 사실 이것은 맞는 동작이긴 합니다. 왜냐하면 비동기 읽기/쓰기를 위해 ReadFile/WriteFile에 전달하는 NativeOverlapped 구조체에는 이미 OffsetLow, OffsetHigh 필드를 통해 (FileStream.Position에 해당하는) 위치를 지정하기 때문입니다.<br /> <br /> 그래서, 정확한 결과를 보려면 위와 같은 소스 코드에서 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 (SafeFileHandle pHandle = NativeMethods.CreateFile(@"c:\temp\test.txt", (uint)FileAccess.ReadWrite, 0, IntPtr.Zero, (uint)2, NativeMethods.FILE_FLAG_OVERLAPPED, IntPtr.Zero)) { WriteAsync(pHandle, <span style='color: blue; font-weight: bold'>0</span>, buf); Thread.Sleep(1000); WriteAsync(pHandle, <span style='color: blue; font-weight: bold'>2</span>, buf2); } private static void WriteAsync(SafeFileHandle pHandle, <span style='color: blue; font-weight: bold'>long pos</span>, byte[] buf) { //...[생략]... NativeOverlapped o = new NativeOverlapped(); <span style='color: blue; font-weight: bold'>o.OffsetLow = unchecked((int)pos); o.OffsetHigh = (int)(pos >> 32);</span> //...[생략]... } // test.txt 파일 내용: "ttee" </pre> <br /> 재미있는 것은 우리가 사용하는 FileStream.WriteAsync의 Read/Write는 별도의 File Position을 전달하지 않습니다.<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.Text; internal class Program { static async Task Main(string[] args) { string filePath = @"C:\temp\test.txt"; File.Delete(filePath); using (FileStream fs = File.OpenWrite(filePath)) { byte[] buffer = Encoding.UTF8.GetBytes("tt"); byte[] buffer2 = Encoding.UTF8.GetBytes("ee"); await fs.WriteAsync(buffer, 0, buffer.Length); await fs.WriteAsync(buffer2, 0, buffer2.Length); // test.txt 파일 내용 // ttee } } } </pre> <br /> 왜냐하면 내부적으로 "_pos"라는 필드를 FileStream에서 관리를 하기 때문입니다. 여기서 더욱 재미있는 것은, FileStream은 최대한 _pos와 FILE 자원의 포인터 위치를 맞추려고 노력한다는 점입니다. 그래서 단순히 WriteAsync와 같은 동작을 할 때 _pos 변수만 업데이트하는 것이 아니라, SetFilePointer를 이용해 파일 포인터 위치까지 바꿔줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 참고: <a target='tab' href='https://github.com/microsoft/referencesource/blob/master/mscorlib/system/io/filestream.cs#L2010'>https://github.com/microsoft/referencesource/blob/master/mscorlib/system/io/filestream.cs#L2010</a> intOverlapped->OffsetLow = unchecked((int)<span style='color: blue; font-weight: bold'>_pos</span>); intOverlapped->OffsetHigh = (int)(_pos>>32); <span style='color: blue; font-weight: bold'>SeekCore(numBytes, SeekOrigin.Current);</span> [System.Security.SecuritySafeCritical] private long SeekCore(long offset, SeekOrigin origin) { int hr = 0; long ret = 0; ret = <span style='color: blue; font-weight: bold'>Win32Native.SetFilePointer(_handle, offset, origin, out hr);</span> if (ret == -1) { if (hr == Win32Native.ERROR_INVALID_HANDLE) _handle.Dispose(); __Error.WinIOError(hr, String.Empty); } <span style='color: blue; font-weight: bold'>_pos = ret;</span> return ret;} } </pre> <br /> 그런데, 위에서 보면 약간의 허점이 있긴 합니다. 즉, 어쨌든 비동기 I/O의 파일 위치는 _pos 변수를 기준으로 하기 때문에 파일 포인터의 위치와는 무관하다는 점입니다. 그래서, 만약 해당 파일에 대해 직접 SetFilePointerEx 함수를 이용해 파일 포인터를 변경하는 것은,<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) { string filePath = @"C:\temp\test.txt"; File.Delete(filePath); using (FileStream fs = File.OpenWrite(filePath)) { byte[] buffer = Encoding.UTF8.GetBytes("tt"); byte[] buffer2 = Encoding.UTF8.GetBytes("ee"); await fs.WriteAsync(buffer, 0, buffer.Length); <span style='color: blue; font-weight: bold'>NativeMethods.SetFilePointerEx(fs.SafeFileHandle, 0, out _, NativeMethods.FILE_BEGIN);</span> await fs.WriteAsync(buffer2, 0, buffer2.Length); } } /* test 파일 출력 결과 ttee */ </pre> <br /> WriteAsync에 아무런 효력도 없습니다. 반면, FileStream이 제공하는 Position 필드를 이용해 위치를 바꾸는 것은,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > await fs.WriteAsync(buffer, 0, buffer.Length); fs.Position = 0; // 내부적으로 _pos 필드를 업데이트하기 때문에, await fs.WriteAsync(buffer2, 0, buffer2.Length); /* test 파일 출력 결과 ee */ </pre> <br /> 효력을 발휘합니다. 은근슬쩍 복잡한 상황들이 전개됩니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <a name='myasyncfilestream'></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;' > public class MyAsyncFileStream : IDisposable { <span style='color: blue; font-weight: bold'>long _pos;</span> SafeFileHandle? _pHandle; public MyAsyncFileStream(string filePath) { _pHandle = NativeMethods.CreateFile(@"c:\temp\test.txt", (uint)FileAccess.ReadWrite, 0, IntPtr.Zero, (uint)2, NativeMethods.FILE_FLAG_OVERLAPPED, IntPtr.Zero); } <span style='color: blue; font-weight: bold'>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; } }</span> public void WriteAsync(byte[] buf) { uint written; NativeOverlapped o = new NativeOverlapped(); <span style='color: blue; font-weight: bold'>o.OffsetLow = unchecked((int)_pos); o.OffsetHigh = (int)(_pos >> 32);</span> <span style='color: blue; font-weight: bold'>SeekCore(buf.Length, SeekOrigin.Current);</span> EventWaitHandle writeEvent = new EventWaitHandle(false, EventResetMode.AutoReset); o.EventHandle = writeEvent.SafeWaitHandle.DangerousGetHandle(); if (NativeMethods.WriteFile(_pHandle!, buf, buf.Length, out written, ref o) == false) { int lastError = Marshal.GetLastWin32Error(); if (lastError == 997) // ERROR_IO_PENDING == 997 { OverlappedParameter op = new OverlappedParameter() { Event = writeEvent, }; op.WaitHandle = ThreadPool.RegisterWaitForSingleObject(writeEvent, WriteCompleted, op, -1, false); op.TryUnregister(); } else { // Write File Error Console.WriteLine("Write File Error"); writeEvent.Close(); } } } public static void WriteCompleted(object? objState, bool timedOut) { Console.WriteLine($"{DateTime.Now} async WriteCompleted"); OverlappedParameter? op = objState as OverlappedParameter; if (op == null) { return; } op.Done(); } public void Dispose() { if (_pHandle != null) { _pHandle.Dispose(); _pHandle = null; } } } public class OverlappedParameter { // ...[생략]... } </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;' > static void Main(string[] args) { byte[] buffer = Encoding.UTF8.GetBytes("tt"); byte[] buffer2 = Encoding.UTF8.GetBytes("ee"); File.Delete(@"C:\temp\test.txt"); using (MyAsyncFileStream fs = new MyAsyncFileStream("C:\temp\test.txt")) { fs.WriteAsync(buffer); Console.WriteLine(fs.Position); // 출력 결과: 2 fs.WriteAsync(buffer2); } Console.WriteLine("Press Enter to exit ..."); Console.ReadLine(); } /* test.txt 파일 출력 결과 ttee */ </pre> <br /> FileStream처럼 별도의 파일 위치를 관리할 필요 없이 순차적으로 read/write 연산을 할 수 있습니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1934&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1321
(왼쪽의 숫자를 입력해야 합니다.)