Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - FILE_FLAG_OVERLAPPED가 적용된 파일의 읽기/쓰기 시 Position 관리

아래의 글을,

C# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법
; https://www.sysnet.pe.kr/2/0/13059

이렇게 실습해 보면,

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))
        {
            WriteAsync(pHandle, buffer);
            Thread.Sleep(1000);
            WriteAsync(pHandle, buffer2);
        }

        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
{
    // ...[생략]...
}

2번의 WriteAsync 호출에서 전달한 2바이트 내용이,

WriteAsync(pHandle, buf); // buf == 'tt'
Thread.Sleep(1000);
WriteAsync(pHandle, buf2); // buf2 == 'ee'

"FileStream.Position == 0"인 위치에 써지기 때문에 출력 파일의 결과는 "ee"만 담고 있게 됩니다. 실제로 FILE_FLAG_OVERLAPPED가 적용된 파일은 Read/Write 동작을 한 후 파일 포인터를 전혀 변경하지 않습니다. 가령, 다음과 같이 SetFilePointerEx Win32 API를 호출해 현재의 파일 포인터 위치를 구해봐도,

WriteAsync(pHandle, buf);
Thread.Sleep(1000);
Console.WriteLine(GetCurrentPosition(pHandle)); // 출력 결과: 0

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;
}

0이 나옵니다. 좀 이상한가요? ^^




사실 이것은 맞는 동작이긴 합니다. 왜냐하면 비동기 읽기/쓰기를 위해 ReadFile/WriteFile에 전달하는 NativeOverlapped 구조체에는 이미 OffsetLow, OffsetHigh 필드를 통해 (FileStream.Position에 해당하는) 위치를 지정하기 때문입니다.

그래서, 정확한 결과를 보려면 위와 같은 소스 코드에서 WriteAsync 메서드에 데이터가 쓰일 위치를 함께 전달해야 합니다.

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, 0, buf);
    Thread.Sleep(1000);
    WriteAsync(pHandle, 2, buf2);
}

private static void WriteAsync(SafeFileHandle pHandle, long pos, byte[] buf)
{
    //...[생략]...
    NativeOverlapped o = new NativeOverlapped();

    o.OffsetLow = unchecked((int)pos);
    o.OffsetHigh = (int)(pos >> 32);

    //...[생략]...
}

// test.txt 파일 내용: "ttee"

재미있는 것은 우리가 사용하는 FileStream.WriteAsync의 Read/Write는 별도의 File Position을 전달하지 않습니다.

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
        }
    }
}

왜냐하면 내부적으로 "_pos"라는 필드를 FileStream에서 관리를 하기 때문입니다. 여기서 더욱 재미있는 것은, FileStream은 최대한 _pos와 FILE 자원의 포인터 위치를 맞추려고 노력한다는 점입니다. 그래서 단순히 WriteAsync와 같은 동작을 할 때 _pos 변수만 업데이트하는 것이 아니라, SetFilePointer를 이용해 파일 포인터 위치까지 바꿔줍니다.

// 참고: https://github.com/microsoft/referencesource/blob/master/mscorlib/system/io/filestream.cs#L2010

intOverlapped->OffsetLow = unchecked((int)_pos);
intOverlapped->OffsetHigh = (int)(_pos>>32);

SeekCore(numBytes, SeekOrigin.Current);

[System.Security.SecuritySafeCritical]
private long SeekCore(long offset, SeekOrigin origin) {
    int hr = 0;
    long ret = 0;
            
    ret = Win32Native.SetFilePointer(_handle, offset, origin, out hr);
    if (ret == -1) {
        if (hr == Win32Native.ERROR_INVALID_HANDLE)
            _handle.Dispose();
        __Error.WinIOError(hr, String.Empty);
    }
            
    _pos = ret;
    return ret;}
}

그런데, 위에서 보면 약간의 허점이 있긴 합니다. 즉, 어쨌든 비동기 I/O의 파일 위치는 _pos 변수를 기준으로 하기 때문에 파일 포인터의 위치와는 무관하다는 점입니다. 그래서, 만약 해당 파일에 대해 직접 SetFilePointerEx 함수를 이용해 파일 포인터를 변경하는 것은,

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);

        NativeMethods.SetFilePointerEx(fs.SafeFileHandle, 0, out _, NativeMethods.FILE_BEGIN);

        await fs.WriteAsync(buffer2, 0, buffer2.Length);
    }
}

/* test 파일 출력 결과
ttee
*/

WriteAsync에 아무런 효력도 없습니다. 반면, FileStream이 제공하는 Position 필드를 이용해 위치를 바꾸는 것은,

await fs.WriteAsync(buffer, 0, buffer.Length);

fs.Position = 0; // 내부적으로 _pos 필드를 업데이트하기 때문에,

await fs.WriteAsync(buffer2, 0, buffer2.Length);

/* test 파일 출력 결과
ee
*/

효력을 발휘합니다. 은근슬쩍 복잡한 상황들이 전개됩니다. ^^




자, 그럼 최종적으로 우리가 구현하는 비동기 파일 연산도 별도의 클래스를 만들어 대충 이런 식으로 구색을 낼 수 있습니다.

public class MyAsyncFileStream : IDisposable
{
    long _pos;
    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);
    }

    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 void WriteAsync(byte[] buf)
    {
        uint written;
        NativeOverlapped o = new NativeOverlapped();
        o.OffsetLow = unchecked((int)_pos);
        o.OffsetHigh = (int)(_pos >> 32);

        SeekCore(buf.Length, SeekOrigin.Current);

        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
{
    // ...[생략]...
}

그리고 사용을 이렇게 하면,

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
*/

FileStream처럼 별도의 파일 위치를 관리할 필요 없이 순차적으로 read/write 연산을 할 수 있습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/16/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13327정성태4/19/20234052VS.NET IDE: 183. C# - .NET Core/5+ 환경에서 Fakes를 이용한 단위 테스트 방법
13326정성태4/18/20235591.NET Framework: 2109. C# - 닷넷 응용 프로그램에서 SQLite 사용 (System.Data.SQLite) [1]파일 다운로드1
13325정성태4/18/20234795스크립트: 48. 파이썬 - PostgreSQL의 with 문을 사용한 경우 연결 개체 누수
13324정성태4/17/20234621.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234573개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20235408VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20234198개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20234085개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234624개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234480개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234509오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233776오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20234121Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20234226개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20234233개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20234303Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234725C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20234370C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234494.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20234455스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20234191.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20234102Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20234411Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234723VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20234046Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234651Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...