. 멀티 스레드로 전환

파일에서 찾기와 바꾸기 기능이 잘 동작하기는 하지만 검색 시간이 오래 걸려서 좀 불만족스럽다. 기계적으로 동작하는 하드디스크를 뒤지는 작업은 어쩔 수 없이 시간이 오래 걸릴 수밖에 없으며 어떠한 방법으로도 절대적인 검색 속도를 개선할 수는 없다. 하지만 시간이 오래 걸린다고 해서 검색이 끝날 때까지 반드시 기다려야만 하는 것은 아니므로 파일검색 작업을 별도의 스레드로 분리하도록 하자.

Win32는 멀티 스레드라는 훌륭한 백그라운드 작업 지원 기능을 제공하는데 파일검색이야말로 스레드를 쓰기에 가장 적합한 작업이다. 처음부터 스레드를 쓰지 않은 이유는 파일검색 절차가 워낙 복잡해서 조금이나마 이해를 돕기 위해 간단한 루틴을 설계하기 위해서였다. 이제 논리가 완전히 만들어졌고 필요한 함수들이 모두 구성되었으므로 멀티 스레드로 전환해보도록 하자. 파일검색에서의 멀티 스레드를 지원하기 위해 스레드 핸들을 전역변수로 선언한다.

 

HANDLE hFIFThread;

 

파일검색이 시작되는 시점은 FindInFiles 함수를 처음 호출할 때이므로 이 함수 호출문을 분리된 스레드로 옮겨 주면 된다. 검색을 시작하는 함수를 다음과 같이 수정한다.

 

void FindOrReplaceInFiles(BOOL bReplace)

{

     BOOL bDeep;

    DWORD ThreadID;

 

     ....

 

    if (bReplace) {

        lstrcpy(LastFIF,arFind[1].Get(0));

        hFIFThread=CreateThread(NULL, 0, FIFThread,(LPVOID)OnReplaceFile,0,&ThreadID);

    } else {

        lstrcpy(LastFIF,arFind[0].Get(0));

        hFIFThread=CreateThread(NULL, 0, FIFThread,(LPVOID)OnFindFile,0,&ThreadID);

     }

}

 

FindInFiles 함수를 직접 호출하는 대신 이 함수를 호출하는 스레드만 시작하도록 했다. 스레드의 인수로 콜백 함수의 포인터를 전달하여 검색중에 발견된 파일에 대해 어떤 처리를 할 것인가를 지정한다. 이제 FindOrReplaceInFiles 함수는 스레드만 시작시켜 놓고 즉시 리턴하여 사용자가 다른 작업을 할 수 있도록 할 것이며 파일검색은 스레드가 담당한다. 스레드 함수는 다음과 같이 작성하였다.

 

DWORD WINAPI FIFThread(LPVOID pCallback)

{

     bContFIF=TRUE;

     FindInFiles(arFind[2].Get(0),arFind[3].Get(0),FIF_DEEP | FIF_INCHID,

          (FIFCALLBACK)pCallback,(LPVOID)NULL);

     return 0;

}

 

FindInFiles가 검색을 계속 하도록 bContFIF 플래그를 TRUE로 만들고 FindInFiles 함수를 호출하였다. bContFIF 전역변수는 TRUE로 초기화되어 있지만 이전 검색 동작이 중간에 취소되면 FALSE로 바뀔 수도 있기 때문에 FindInFiles를 호출하기 전에 무조건 이 변수를 TRUE로 다시 바꿔 주는 것이 안전하다. 이렇게 되면 FindInFiles 함수가 분리된 스레드에서 실행되므로 메인 스레드는 파일검색을 하는 중에도 다른 작업을 계속 할 수 있다.

파일을 열어서 편집할 수도 있고 편집하던 파일을 저장할 수도 있으며 문서 내에서 검색도 가능하다. 심지어 검색결과를 볼 수도 있고 검색결과에서 파일을 찾아가는 것도 가능하다. 하지만 딱 한 가지 할 수 없는 작업이 하나 있는데 파일을 검색하는 중에 파일 찾기는 할 수 없다. 한 스레드를 여러 번 실행하는 것도 원칙적으로 가능하지만 출력 결과창이 하나밖에 없기 때문에 두 검색 스레드의 검색결과가 한 결과창에 섞여서 출력될 것이다. 출력 결과창이 여러 개라면 동시에 두 개의 파일검색 스레드를 돌리더라도 속도만 느려질 뿐 논리적으로 아무 문제가 없다.

만약 파일검색을 하는 중에 또 다른 파일검색 명령을 내렸다면 이때는 사용자에게 현재 진행중인 검색을 중단할 것인지 질문해야 한다. 사용자가 검색 중단을 선택하면 즉시 실행중인 스레드를 종료하고 새로운 검색을 위한 파일 찾기 대화상자를 띄워주고 검색 계속을 선택하면 진행중인 검색이 계속 실행될 수 있도록 아무 일도 하지 않으면 된다. 다음 함수는 파일검색 스레드의 현재 상태를 조사하고 실행중이라면 사용자에게 질문을 한다.

 

BOOL TestFIFThread()

{

     DWORD dwExit;

     MSG Message;

 

     GetExitCodeThread(hFIFThread,&dwExit);

     if (dwExit==STILL_ACTIVE) {

          if (MessageBox(g_hFrameWnd,"파일 검색이 이미 진행중입니다. "

              "검색을 중지하고 새로운 검색을 하시겠습니까?","질문",MB_YESNO)==IDNO)

              return FALSE;

          bContFIF=FALSE;

          while (WaitForSingleObject(hFIFThread,0)==WAIT_TIMEOUT) {

              if (PeekMessage(&Message,NULL,0,0,PM_REMOVE)) {

                   DispatchMessage(&Message);

              }

          }

     }

     return TRUE;

}

 

GetExitCodeThread 함수는 스레드의 종료 상태를 조사하는데 만약 아직 종료되지 않은 스레드라면 STILL_ACTIVE가 리턴된다. 이 경우 이미 진행중인 검색을 중지할 것인지 질문하고 예를 선택하면 즉시 실행중인 스레드를 중단시켜야 한다. FindInFiles 함수를 중간에 종료시키려면 bContFIF 플래그를 FALSE로 바꿔 주기만 하면 된다. TerminateThread 함수를 사용하여 스레드를 강제로 종료시킬 수도 있지만 바람직하지 않으므로 스레드가 스스로 종료할 수 있도록 신호를 주는 것이 더 낫다. 보통 스레드의 종료를 위해서는 이벤트 객체를 많이 사용하지만 이 예제에서와 같이 전역변수를 쓰는 방법도 간편하다는 점에서 나쁜 선택이 아니다.

FindInFiles 함수는 파일 하나를 검사할 때마다 bContFIF 플래그를 점검하도록 되어 있지만 이 플래그를 FALSE로 바꾼다고 해서 즉시 종료되는 것은 아니다. 한 파일에 대한 검색을 진행중이라면 이 파일검색을 완전히 마치고 다음 파일을 검색하기 직전에 bContFIF 플래그를 보게 되므로 스레드가 완전히 종료될 때까지는 약간의 시간이 더 필요하다. bContFIF FALSE로 바꾸는 것은 메인 스레드가 파일검색 스레드에게 ! 당장 그만 둬라는 지시를 하는 것인데 FindInFiles 함수가 이 지시를 그다지 빨리 눈치채지 못하는 것이다.

그래서 메인 스레드는 bContFIF FALSE로 바꾼 후 파일검색 스레드가 완전히 종료될 때까지 대기해야 한다. 이때 WaitForSingleObject 함수로 단순히 대기만 해서는 안된다. 파일검색 스레드는 검색결과 출력을 위해 ListView_InsertItem 같은 함수를 호출하여 리스트 뷰에 문자열을 삽입하는데 이 함수 호출은 결국은 메인 스레드가 처리해야 하는 SendMessage호출이다. SendMessage 호출은 완전히 리턴될 때까지는 스레드를 블록시키는 특징이 있다. 현재 메인 스레드는 TestFIFThread 함수를 실행하고 있기 때문에 파일검색 스레드의 SendMessage 호출은 TestFIFThread 함수가 종료될 때까지는 처리되지 못한다.

결국 파일검색 스레드는 메인 스레드가 SendMessage 요청을 처리하기를 기다리게 되고 메인 스레드는 파일검색 스레드의 종료를 기다리게 되므로 전형적인 데드락(DeadLock) 상황이 되는 것이다. 그래서 메인 스레드는 자신에게 전달되는 메시지는 처리하면서 대기를 해야 한다. WaitForSingleObject 함수로 파일검색 스레드의 상태를 조사만 하고 아직 신호상태가 아니면 메시지 펌핑을 하면서 이 스레드가 완전히 종료될 때까지 기다리도록 하였다.

TestFIFThread 함수는 파일검색 스레드가 실행중이 아니거나 사용자에 의해 중지되었으면 TRUE를 리턴하고 실행을 계속해야 할 상황이면 FALSE를 리턴한다. OnCommand에서는 파일검색을 시작하기 전에 이 함수를 먼저 호출해보고 새로운 검색을 시작해도 될 상황인지를 먼저 조사한 후 파일 찾기 대화상자를 띄운다. 코드를 다음과 같이 수정하도록 하자.

 

void OnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     ....

     case IDM_SEARCH_FILES:

          if (TestFIFThread()==TRUE) {

              if (DialogBox(g_hInst,MAKEINTRESOURCE(IDD_FILEFIND),hWnd,FileFindDlgProc)==IDOK) {

                   FindOrReplaceInFiles(FALSE);

              }

          }

          break;

     case IDM_SEARCH_RFILES:

          if (TestFIFThread()==TRUE) {

              if (DialogBox(g_hInst,MAKEINTRESOURCE(IDD_FILEREPLACE),hWnd,FileFindDlgProc)==IDOK) {

                   FindOrReplaceInFiles(TRUE);

              }

          }

          break;

 

여기까지 코드를 작성한 후 예제를 실행해보면 백그라운드 검색이 가능할 것이다. 검색중에 딴 짓을 할 수도 있고 실행중인 검색을 취소할 수도 있다. 기능상으로는 완성되었지만 검색 취소의 반응성이 좋지 못하므로 조금 더 빨리 취소할 수 있도록 해야 한다.

bContFIF 플래그는 FindInFiles 함수에서 다음 대상 파일로 옮겨갈 때 딱 한 번만 점검된다. 그래서 OnFindFile이 이미 호출된 상황일 때는 이 함수가 완전히 리턴할 때까지 스레드를 종료하라는 신호를 접수하지 못하고 계속 실행한다. 만약 한 파일의 크기가 10MB이고 이 파일에 검색 대상 문자열이 1000개가 있다면 이 파일을 다 검색할 때까지 스레드는 메인 스레드의 종료 지시를 알지 못한 채로 계속 실행된다.

이 문제를 해결하려면 bContFIF 플래그를 좀 더 자주 점검하여 스레드를 끝낼 시기를 빨리 알아챌 수 있도록 해야 한다. 반복적인 루프의 중간중간에 이 변수 점검문을 삽입하도록 하자. 다음 두 함수가 가장 자주 호출되므로 이 함수의 루프 끝에서 bContFIF를 점검해보도록 하였다.

 

int OnFindFile(TCHAR *Path,DWORD Attr,LPVOID pCustom)

{

     ....

     for (pbuf=buf;;) {

          ....

        if (bContFIF==FALSE)

           break;

     }

 

     free(buf);

     CloseHandle(hFile);

     return 0;

}

 

int OnReplaceFile(TCHAR *Path, DWORD Attr, LPVOID pCustom)

{

     ....

     for (pbuf=buf;;) {

          ....

        if (bContFIF==FALSE) {

           bReplace=FALSE;

           break;

        }

     }

 

     CloseHandle(hFile);

     if (bReplace) {

          hFile=CreateFile(Path,GENERIC_WRITE,0,NULL,

              CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

          if (hFile!=INVALID_HANDLE_VALUE) {

              WriteFile(hFile,buf,lstrlen(buf),&dwRead,NULL);

              CloseHandle(hFile);

          }

     }

 

     free(buf);

     return 0;

}

 

한 파일에 대해 찾기나 바꾸기를 하던 중에라도 메인 스레드가 중지를 명령하면 이 루프에서 bContFIF를 점검해보고 즉시 작업을 중단하도록 했다. 하던 작업을 중단한다고 해서 바로 return해서는 안되며 마지막 뒤처리는 하고 리턴해야 한다. , 파일 바꾸기의 경우 한 파일의 일부만 변경되는 것을 방지하기 위해 bReplace는 강제로 FALSE로 바꾸어 중간에 취소된 파일은 바꾸기를 하지 않는다.