Microsoft MVP성태의 닷넷 이야기
VC++: 42. 쓰기 전용 파일(예: 로그 파일)의 동기화 방법 [링크 복사], [링크+제목 복사],
조회: 29475
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

쓰기 전용 파일(예: 로그 파일)의 동기화 방법

서문 필요 없이 바로 코드로 설명해보면, 보통 로그 파일을 쓰기 위한 코드는 아래와 같이 작성됩니다.

HANDLE hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
    ::SetFilePointerEx(hFile, ..., FILE_END);
    ::WriteFile(hFile, ...);
    CloseHandle(hFile); 
}

그런데, 스레드가 끼어들면 - 여러분들은 어떻게 하셨는지 모르겠지만 - 제 경우에는 다음과 같이 꼭!!! 언제나 스레드 동기화를 해주었습니다.

EnterCriticalSection(...);

HANDLE hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
    ...
}

LeaveCriticalSection(...);

만약 스레드가 아니라, 프로세스 간에 하나의 파일에 로그를 쓰려고 하는 경우가 발생하면 뮤텍스로 동기화를 해주었습니다.

WaitForSingleObject(mutexHandle,  INFINITE); 

HANDLE hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
    ...
}

ReleaseMutex(mutexHandle);

그런데,,, 동기화 작업은 쓸데없는 코드에 불과했습니다. 이유는? ^^




최근에 정말 재미있게 읽고 있는 책인데요.

IT EXPERT 윈도우 디바이스 드라이버 (저자 : 이봉석)
; http://www.yes24.com/24/goods/3219701

아주 재미있는 것을 하나 배웠습니다. 바로, ReadFile/WriteFile은 그 동작 자체에 이미 "직렬화" 과정이 포함되어 있다는 것입니다.

사실, 그동안 제가 생각 없이 살았던 것이지요. 어쩌면 굳이 저 책을 읽지 않았어도 '상식적'인 수준에서 평소에도 알 수 있었을 만한 것입니다. 왜냐하면, WriteFile 요청이 2개가 전달되었을 때 하드 디스크에 하나씩 전달되어 처리가 되는 것이 맞을 것입니다. 그렇지 않고, 첫 번째 WriteFile 요청으로 전달된 바이트 배열의 일부만을 디스크에 기록하다가 두 번째로 전달된 WriteFile 요청을 처리한다는 것은 '상식적'으로 맞지 않는 동작 방식입니다.

아하... 정말 그렇다면, "하나의 EXE 프로세스"에서 생성된 "다중 스레드"들은 굳이 CriticalSection을 사용할 필요도 없고, 핸들을 모든 스레드 간에 공유해도 전혀 문제가 안됩니다.


HANDLE g_hFile;
void Main()
{
    g_hFile = ::CreateFile(filePath, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
}

DWORD WINAPI ThreadFunc1(LPVOID lpParam) 
{
    WriteFile(g_hFile, ...);
    ...
}

그런데, 핸들을 공유하지 않고 매번 파일을 열고 닫는 식으로 사용해야 한다면 CriticalSection을 사용해야 합니다. 왜냐하면, "쓰기" 작업 전에 파일 포인터를 끝으로 옮겨야 하는 동작까지를 하나의 atomic 작업으로 묶어주어야 하기 때문입니다. 대신 임계영역을 묶어주는 범위가 줄어듭니다.

DWORD WINAPI ThreadFunc1(LPVOID lpParam) 
{
    HANDLE hFile = ::CreateFile(filePath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile != INVALID_HANDLE_VALUE)
    {
        ::EnterCriticalSection(&critic);

        ::SetFilePointerEx(hFile, ..., FILE_END);
        ::WriteFile(hFile, ...);

        ::LeaveCriticalSection(&critic);
        CloseHandle(hFile); 
    }
}

그런데, 더욱 깔끔한 방법이 있습니다. SetFilePointerEx 작업을 생략하기 위해 파일 자체를 append 모드로 열면 되는데, 다음과 같이 FILE_APPEND_DATA 플래그가 이런 경우에 사용됩니다.

DWORD WINAPI ThreadFunc1(LPVOID lpParam) 
{
    HANDLE hFile = ::CreateFile(filePath, FILE_APPEND_DATA,
        FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile != INVALID_HANDLE_VALUE)
    {
        ::WriteFile(hFile, ...);
        CloseHandle(hFile); 
    }
}




자,,, 그럼 2개 이상의 프로세스에서 하나의 파일에 대해 로그 작업을 남기는 경우라면 어떻게 될까요? 이 경우에는 FILE_APPEND_DATA를 쓰느냐/안쓰느냐에 따라서 달라집니다. 안 쓰는 경우에는 SetFilePointer/WriteFile 단위를 Mutex로 묶어주어야 합니다.

하지만, FILE_APPEND_DATA를 사용하게 되면 Mutex가 필요하지 않습니다. 즉, 단일 프로세스에서 로그 작업을 하는 것과 동일하게 다중 프로세스에서도 코드를 그대로 사용할 수 있습니다.

그럼, 간단히 아래와 같이 정리되었군요.

  • 동기화 작업을 하지 말고,
  • FILE_APPEND_DATA 모드로 파일을 연다.

여기까지는, Win32 API를 직접 사용한 경우의 이야기이고, 그렇다면 .NET Framework에서는 어떻게 될까요?

아쉽게도, .NET에서는 FileStream의 생성자에 FILE_APPEND_DATA와 동일한 기능을 하는 옵션을 줄 수가 없습니다. 이 때문에, P/Invoke로 직접 Win32 API의 CreateFile로 파일 핸들을 생성하고 그것을 FileStream에 전달해 주어야 합니다.

SafeFileHandle fileHandle = CreateFile(filePath, NativeFileAccess.FILE_APPEND_DATA, 
                NativeFileShare.FILE_SHARE_READ | NativeFileShare.FILE_SHARE_WRITE, 
                IntPtr.Zero, NativeFileMode.OPEN_ALWAYS, 
                NativeFileFlag.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);

using (file = new FileStream(fileHandle, FileAccess.Write))
{
    file.Write( ... );
}

첨부한 파일에는 위의 Win32 API와 .NET을 테스트한 예제 프로젝트가 들어 있습니다.

----------------

* 이래서... 공부는 끝이 없는 것 같습니다. ^^ 사실 이 맛에 또 공부하고!

* SetFilePointer(hFile, 0, 0, FILE_END);는 4GB가 넘는 파일에 대해서 정상적으로 파일 끝을 가리키지 못합니다. 대용량 파일을 다뤄야 할 필요가 있는 경우에는 필히 SetFilePointerEx를 사용하십시오. ^^




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







[최초 등록일: ]
[최종 수정일: 7/10/2021]

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

비밀번호

댓글 작성자
 




... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11258정성태8/1/201720314.NET Framework: 668. 지연 서명된 DLL과 서명된 DLL의 차이점파일 다운로드1
11257정성태7/31/201719898.NET Framework: 667. bypassTrustedAppStrongNames 옵션 설명파일 다운로드1
11256정성태7/25/201721832디버깅 기술: 90. windbg의 lm 명령으로 보이지 않는 .NET 4.0 ClassLibrary를 명시적으로 로드하는 방법 [1]
11255정성태7/18/201724343디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201720767개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201721403Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201719134오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201718619디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201722215디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201719962오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
11248정성태7/12/201726520오류 유형: 409. pip install pefile - 'cp949' codec can't decode byte 0xe2 in position 208687: illegal multibyte sequence
11247정성태7/12/201720838오류 유형: 408. SqlConnection 객체 생성 시 무한 대기 문제파일 다운로드1
11246정성태7/11/201718882VS.NET IDE: 118. Visual Studio - 다중 폴더에 포함된 파일들에 대한 "Copy to Output Directory"를 한 번에 설정하는 방법
11245정성태7/10/201724628개발 환경 구성: 321. Visual Studio Emulator for Android 소개 [2]
11244정성태7/10/201724814오류 유형: 407. Visual Studio에서 ASP.NET Core 실행할 때 dotnet.exe 프로세스의 -532462766 오류 발생 [1]
11243정성태7/10/201721591.NET Framework: 666. dotnet.exe - 윈도우 운영체제에서의 .NET Core 버전 찾기 규칙
11242정성태7/8/201721129제니퍼 .NET: 27. 제니퍼 닷넷 적용 사례 (7) - 노후된 스토리지 장비로 인한 웹 서비스 Hang (멈춤) 현상
11241정성태7/8/201719777오류 유형: 406. Xamarin 빌드 에러 XA5209, APT0000
11240정성태7/7/201723593.NET Framework: 665. ClickOnce를 웹 브라우저를 이용하지 않고 쿼리 문자열을 전달하면서 실행하는 방법 [3]파일 다운로드1
11239정성태7/6/201724248.NET Framework: 664. Protocol Handler - 웹 브라우저에서 데스크톱 응용 프로그램을 실행하는 방법 [5]파일 다운로드1
11238정성태7/6/201721776오류 유형: 405. NT 서비스 시작 시 "Error 1067: The process terminated unexpectedly." 오류 발생 [2]
11237정성태7/5/201723435.NET Framework: 663. C# - PDB 파일 경로를 PE 파일로부터 얻는 방법파일 다운로드1
11236정성태7/4/201727162.NET Framework: 662. C# - VHD/VHDX 가상 디스크를 마운트하지 않고 파일을 복사하는 방법파일 다운로드1
11235정성태6/29/201721394Math: 20. Matlab/Octave로 Gram-Schmidt 정규 직교 집합 구하는 방법
11234정성태6/29/201718867오류 유형: 404. SharePoint 2013 설치 과정에서 "The username is invalid The account must be a valid domain account" 오류 발생
11233정성태6/28/201718777오류 유형: 403. SharePoint Server 2013을 Windows Server 2016에 설치할 때 .NET 4.5 설치 오류 발생
... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...