본문으로 건너뛰기
Previous
Next
MFC 멀티스레딩 | CWinThread·동기화 완벽 가이드

MFC 멀티스레딩 | CWinThread·동기화 완벽 가이드

MFC 멀티스레딩 | CWinThread·동기화 완벽 가이드

이 글의 핵심

MFC 멀티스레딩은 AfxBeginThread로 시작합니다. Worker Thread는 백그라운드 작업용, UI Thread는 독립 메시지 루프용입니다. PostMessage로 GUI 스레드와 통신하고, CMutex·CCriticalSection으로 동기화합니다.

들어가며

MFC 멀티스레딩은 AfxBeginThread로 스레드를 생성하고, CWinThread 클래스로 관리합니다. 2000년대 Windows 애플리케이션에서는 백그라운드 작업(파일 다운로드, 데이터 처리 등)을 처리하거나 UI 응답성을 유지하기 위해 멀티스레딩이 필수였습니다.

// 가장 간단한 Worker Thread
UINT WorkerThreadFunc(LPVOID pParam)
{
    for (int i = 0; i < 10; i++) {
        Sleep(1000);
        TRACE(_T("작업 중: %d\n"), i);
    }
    return 0;
}

// 스레드 시작
CWinThread* pThread = AfxBeginThread(WorkerThreadFunc, NULL);

1. Worker Thread

1.1 기본 Worker Thread

// Worker Thread 함수
UINT DownloadThreadFunc(LPVOID pParam)
{
    CString* pURL = (CString*)pParam;
    
    // 다운로드 시뮬레이션
    for (int i = 0; i <= 100; i += 10) {
        Sleep(500);
        TRACE(_T("다운로드 진행: %d%%\n"), i);
    }
    
    delete pURL;  // 파라미터 정리
    return 0;
}

// 다이얼로그에서 시작
void CMyDialog::OnBnClickedDownload()
{
    CString* pURL = new CString(_T("http://example.com/file.zip"));
    
    CWinThread* pThread = AfxBeginThread(
        DownloadThreadFunc,  // 스레드 함수
        pURL,                // 파라미터
        THREAD_PRIORITY_NORMAL,
        0,                   // 스택 크기 (0=기본)
        CREATE_SUSPENDED     // 일시 중지 상태로 생성
    );
    
    if (pThread != NULL) {
        pThread->m_bAutoDelete = TRUE;  // 종료 시 자동 삭제
        pThread->ResumeThread();        // 실행 시작
    }
}

1.2 GUI 업데이트를 위한 PostMessage

#define WM_DOWNLOAD_PROGRESS (WM_USER + 1)
#define WM_DOWNLOAD_COMPLETE (WM_USER + 2)

struct DownloadParam {
    HWND hwndNotify;  // 알림받을 윈도우
    CString url;
};

UINT DownloadThreadFunc(LPVOID pParam)
{
    DownloadParam* pDownload = (DownloadParam*)pParam;
    
    for (int i = 0; i <= 100; i += 10) {
        Sleep(500);
        
        // 진행률 알림
        ::PostMessage(pDownload->hwndNotify, WM_DOWNLOAD_PROGRESS, i, 0);
    }
    
    // 완료 알림
    ::PostMessage(pDownload->hwndNotify, WM_DOWNLOAD_COMPLETE, 0, 0);
    
    delete pDownload;
    return 0;
}

// 다이얼로그
class CMyDialog : public CDialog
{
protected:
    CProgressCtrl m_progress;
    
    DECLARE_MESSAGE_MAP()
    
    afx_msg LRESULT OnDownloadProgress(WPARAM wParam, LPARAM lParam)
    {
        int progress = (int)wParam;
        m_progress.SetPos(progress);
        
        CString text;
        text.Format(_T("다운로드 중: %d%%"), progress);
        SetDlgItemText(IDC_STATIC_STATUS, text);
        
        return 0;
    }
    
    afx_msg LRESULT OnDownloadComplete(WPARAM wParam, LPARAM lParam)
    {
        AfxMessageBox(_T("다운로드 완료!"));
        GetDlgItem(IDC_BTN_DOWNLOAD)->EnableWindow(TRUE);
        return 0;
    }
    
    afx_msg void OnBnClickedDownload()
    {
        GetDlgItem(IDC_BTN_DOWNLOAD)->EnableWindow(FALSE);
        
        DownloadParam* pParam = new DownloadParam;
        pParam->hwndNotify = m_hWnd;
        pParam->url = _T("http://example.com/file.zip");
        
        AfxBeginThread(DownloadThreadFunc, pParam);
    }
};

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
    ON_MESSAGE(WM_DOWNLOAD_PROGRESS, &CMyDialog::OnDownloadProgress)
    ON_MESSAGE(WM_DOWNLOAD_COMPLETE, &CMyDialog::OnDownloadComplete)
    ON_BN_CLICKED(IDC_BTN_DOWNLOAD, &CMyDialog::OnBnClickedDownload)
END_MESSAGE_MAP()

2. UI Thread

2.1 독립 윈도우를 가진 UI Thread

// UI Thread 클래스
class CMyUIThread : public CWinThread
{
    DECLARE_DYNCREATE(CMyUIThread)
    
protected:
    CMyUIThread();
    
public:
    virtual BOOL InitInstance();
    virtual int ExitInstance();
    
    DECLARE_MESSAGE_MAP()
};

IMPLEMENT_DYNCREATE(CMyUIThread, CWinThread)

CMyUIThread::CMyUIThread()
{
}

BOOL CMyUIThread::InitInstance()
{
    // 독립 윈도우 생성
    CFrameWnd* pFrame = new CFrameWnd;
    pFrame->Create(NULL, _T("UI Thread Window"));
    
    m_pMainWnd = pFrame;
    pFrame->ShowWindow(SW_SHOW);
    pFrame->UpdateWindow();
    
    return TRUE;
}

int CMyUIThread::ExitInstance()
{
    return CWinThread::ExitInstance();
}

BEGIN_MESSAGE_MAP(CMyUIThread, CWinThread)
END_MESSAGE_MAP()

// 시작
CWinThread* pUIThread = AfxBeginThread(
    RUNTIME_CLASS(CMyUIThread),
    THREAD_PRIORITY_NORMAL,
    0,
    CREATE_SUSPENDED
);
pUIThread->m_bAutoDelete = FALSE;  // 수동 삭제
pUIThread->ResumeThread();

3. 동기화 객체

3.1 CCriticalSection

class CSharedData
{
private:
    CCriticalSection m_cs;
    int m_nCounter;
    
public:
    CSharedData() : m_nCounter(0) {}
    
    void Increment()
    {
        CSingleLock lock(&m_cs);
        lock.Lock();
        
        m_nCounter++;
        TRACE(_T("Counter: %d\n"), m_nCounter);
        
        lock.Unlock();
    }
    
    int GetCounter()
    {
        CSingleLock lock(&m_cs);
        lock.Lock();
        
        int value = m_nCounter;
        
        lock.Unlock();
        return value;
    }
};

CSharedData g_data;

UINT WorkerThread1(LPVOID pParam)
{
    for (int i = 0; i < 1000; i++) {
        g_data.Increment();
    }
    return 0;
}

UINT WorkerThread2(LPVOID pParam)
{
    for (int i = 0; i < 1000; i++) {
        g_data.Increment();
    }
    return 0;
}

// 사용
AfxBeginThread(WorkerThread1, NULL);
AfxBeginThread(WorkerThread2, NULL);

Sleep(3000);
TRACE(_T("최종 카운터: %d\n"), g_data.GetCounter());  // 2000

3.2 CMutex

class CSharedResource
{
private:
    CMutex m_mutex;
    CString m_data;
    
public:
    BOOL Write(const CString& data, DWORD dwTimeout = INFINITE)
    {
        CSingleLock lock(&m_mutex);
        
        if (!lock.Lock(dwTimeout)) {
            return FALSE;  // 타임아웃
        }
        
        m_data = data;
        TRACE(_T("Write: %s\n"), data);
        
        lock.Unlock();
        return TRUE;
    }
    
    CString Read(DWORD dwTimeout = INFINITE)
    {
        CSingleLock lock(&m_mutex);
        
        if (!lock.Lock(dwTimeout)) {
            return _T("");
        }
        
        CString result = m_data;
        TRACE(_T("Read: %s\n"), result);
        
        lock.Unlock();
        return result;
    }
};

3.3 CSemaphore (리소스 개수 제한)

class CConnectionPool
{
private:
    CSemaphore m_semaphore;
    
public:
    CConnectionPool(int maxConnections)
        : m_semaphore(maxConnections, maxConnections)  // 초기값, 최대값
    {
    }
    
    BOOL AcquireConnection(DWORD dwTimeout = INFINITE)
    {
        CSingleLock lock(&m_semaphore);
        
        if (lock.Lock(dwTimeout)) {
            TRACE(_T("연결 획득\n"));
            return TRUE;
        } else {
            TRACE(_T("연결 획득 실패 (풀 포화)\n"));
            return FALSE;
        }
    }
    
    void ReleaseConnection()
    {
        m_semaphore.Unlock();
        TRACE(_T("연결 반환\n"));
    }
};

CConnectionPool g_pool(3);  // 최대 3개 연결

UINT ConnectionThread(LPVOID pParam)
{
    int id = (int)pParam;
    
    if (g_pool.AcquireConnection(5000)) {
        TRACE(_T("Thread %d: 작업 중...\n"), id);
        Sleep(2000);  // 작업
        
        g_pool.ReleaseConnection();
        TRACE(_T("Thread %d: 완료\n"), id);
    } else {
        TRACE(_T("Thread %d: 타임아웃\n"), id);
    }
    
    return 0;
}

// 10개 스레드 시작 (3개씩만 동시 실행)
for (int i = 0; i < 10; i++) {
    AfxBeginThread(ConnectionThread, (LPVOID)i);
}

3.4 CEvent (이벤트 신호)

CEvent g_eventStop;  // 수동 리셋 이벤트

UINT MonitorThreadFunc(LPVOID pParam)
{
    while (TRUE) {
        // 100ms마다 체크
        DWORD result = ::WaitForSingleObject(g_eventStop.m_hObject, 100);
        
        if (result == WAIT_OBJECT_0) {
            TRACE(_T("중지 신호 받음\n"));
            break;
        }
        
        // 모니터링 작업
        TRACE(_T("모니터링 중...\n"));
    }
    
    return 0;
}

// 시작
CWinThread* pThread = AfxBeginThread(MonitorThreadFunc, NULL);

// 중지
g_eventStop.SetEvent();  // 신호 발생
WaitForSingleObject(pThread->m_hThread, INFINITE);  // 종료 대기

4. 실전 예제: 파일 다운로드 매니저

// DownloadManager.h
class CDownloadManager
{
private:
    struct DownloadTask {
        int id;
        CString url;
        CString savePath;
        int progress;
        BOOL completed;
    };
    
    CList<DownloadTask*> m_tasks;
    CCriticalSection m_cs;
    CWinThread* m_pThread;
    CEvent m_eventStop;
    HWND m_hwndNotify;
    
    static UINT DownloadThreadFunc(LPVOID pParam);
    
public:
    CDownloadManager(HWND hwndNotify);
    ~CDownloadManager();
    
    int AddDownload(const CString& url, const CString& savePath);
    void Start();
    void Stop();
    BOOL IsRunning();
    
    int GetTaskCount();
    int GetCompletedCount();
};

// DownloadManager.cpp
#define WM_DOWNLOAD_TASK_PROGRESS (WM_USER + 10)
#define WM_DOWNLOAD_TASK_COMPLETE (WM_USER + 11)

CDownloadManager::CDownloadManager(HWND hwndNotify)
    : m_pThread(NULL)
    , m_hwndNotify(hwndNotify)
{
}

CDownloadManager::~CDownloadManager()
{
    Stop();
    
    POSITION pos = m_tasks.GetHeadPosition();
    while (pos) {
        delete m_tasks.GetNext(pos);
    }
}

int CDownloadManager::AddDownload(const CString& url, const CString& savePath)
{
    CSingleLock lock(&m_cs);
    lock.Lock();
    
    DownloadTask* pTask = new DownloadTask;
    pTask->id = (int)m_tasks.GetCount();
    pTask->url = url;
    pTask->savePath = savePath;
    pTask->progress = 0;
    pTask->completed = FALSE;
    
    m_tasks.AddTail(pTask);
    
    lock.Unlock();
    
    return pTask->id;
}

void CDownloadManager::Start()
{
    if (m_pThread != NULL) {
        return;
    }
    
    m_eventStop.ResetEvent();
    m_pThread = AfxBeginThread(DownloadThreadFunc, this);
}

void CDownloadManager::Stop()
{
    if (m_pThread == NULL) {
        return;
    }
    
    m_eventStop.SetEvent();
    ::WaitForSingleObject(m_pThread->m_hThread, INFINITE);
    m_pThread = NULL;
}

UINT CDownloadManager::DownloadThreadFunc(LPVOID pParam)
{
    CDownloadManager* pMgr = (CDownloadManager*)pParam;
    
    while (TRUE) {
        // 중지 확인
        if (::WaitForSingleObject(pMgr->m_eventStop.m_hObject, 0) == WAIT_OBJECT_0) {
            break;
        }
        
        // 다음 작업 찾기
        CSingleLock lock(&pMgr->m_cs);
        lock.Lock();
        
        DownloadTask* pTask = NULL;
        POSITION pos = pMgr->m_tasks.GetHeadPosition();
        while (pos) {
            DownloadTask* p = pMgr->m_tasks.GetNext(pos);
            if (!p->completed) {
                pTask = p;
                break;
            }
        }
        
        lock.Unlock();
        
        if (pTask == NULL) {
            break;  // 모든 작업 완료
        }
        
        // 다운로드 시뮬레이션
        for (int i = 0; i <= 100; i += 10) {
            Sleep(500);
            
            pTask->progress = i;
            
            // 진행률 알림
            ::PostMessage(pMgr->m_hwndNotify, WM_DOWNLOAD_TASK_PROGRESS,
                pTask->id, i);
        }
        
        pTask->completed = TRUE;
        
        // 완료 알림
        ::PostMessage(pMgr->m_hwndNotify, WM_DOWNLOAD_TASK_COMPLETE,
            pTask->id, 0);
    }
    
    return 0;
}

int CDownloadManager::GetTaskCount()
{
    CSingleLock lock(&m_cs);
    lock.Lock();
    
    int count = (int)m_tasks.GetCount();
    
    lock.Unlock();
    return count;
}

int CDownloadManager::GetCompletedCount()
{
    CSingleLock lock(&m_cs);
    lock.Lock();
    
    int count = 0;
    POSITION pos = m_tasks.GetHeadPosition();
    while (pos) {
        if (m_tasks.GetNext(pos)->completed) {
            count++;
        }
    }
    
    lock.Unlock();
    return count;
}

// 다이얼로그에서 사용
class CMyDialog : public CDialog
{
private:
    CDownloadManager* m_pDownloadMgr;
    CListCtrl m_list;
    
protected:
    virtual BOOL OnInitDialog()
    {
        CDialog::OnInitDialog();
        
        m_pDownloadMgr = new CDownloadManager(m_hWnd);
        
        m_list.InsertColumn(0, _T("ID"), LVCFMT_LEFT, 50);
        m_list.InsertColumn(1, _T("URL"), LVCFMT_LEFT, 250);
        m_list.InsertColumn(2, _T("진행률"), LVCFMT_LEFT, 100);
        
        return TRUE;
    }
    
    virtual ~CMyDialog()
    {
        delete m_pDownloadMgr;
    }
    
    DECLARE_MESSAGE_MAP()
    
    afx_msg void OnBnClickedAdd()
    {
        CString url;
        GetDlgItemText(IDC_EDIT_URL, url);
        
        if (!url.IsEmpty()) {
            int id = m_pDownloadMgr->AddDownload(url, _T("C:\\Downloads\\"));
            
            int index = m_list.InsertItem(id, _T(""));
            CString strID;
            strID.Format(_T("%d"), id);
            m_list.SetItemText(index, 0, strID);
            m_list.SetItemText(index, 1, url);
            m_list.SetItemText(index, 2, _T("0%"));
        }
    }
    
    afx_msg void OnBnClickedStart()
    {
        m_pDownloadMgr->Start();
        GetDlgItem(IDC_BTN_START)->EnableWindow(FALSE);
        GetDlgItem(IDC_BTN_STOP)->EnableWindow(TRUE);
    }
    
    afx_msg void OnBnClickedStop()
    {
        m_pDownloadMgr->Stop();
        GetDlgItem(IDC_BTN_START)->EnableWindow(TRUE);
        GetDlgItem(IDC_BTN_STOP)->EnableWindow(FALSE);
    }
    
    afx_msg LRESULT OnDownloadTaskProgress(WPARAM wParam, LPARAM lParam)
    {
        int id = (int)wParam;
        int progress = (int)lParam;
        
        CString text;
        text.Format(_T("%d%%"), progress);
        m_list.SetItemText(id, 2, text);
        
        return 0;
    }
    
    afx_msg LRESULT OnDownloadTaskComplete(WPARAM wParam, LPARAM lParam)
    {
        int id = (int)wParam;
        m_list.SetItemText(id, 2, _T("완료"));
        
        int total = m_pDownloadMgr->GetTaskCount();
        int completed = m_pDownloadMgr->GetCompletedCount();
        
        if (completed == total) {
            AfxMessageBox(_T("모든 다운로드 완료!"));
            GetDlgItem(IDC_BTN_START)->EnableWindow(TRUE);
            GetDlgItem(IDC_BTN_STOP)->EnableWindow(FALSE);
        }
        
        return 0;
    }
};

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
    ON_BN_CLICKED(IDC_BTN_ADD, &CMyDialog::OnBnClickedAdd)
    ON_BN_CLICKED(IDC_BTN_START, &CMyDialog::OnBnClickedStart)
    ON_BN_CLICKED(IDC_BTN_STOP, &CMyDialog::OnBnClickedStop)
    ON_MESSAGE(WM_DOWNLOAD_TASK_PROGRESS, &CMyDialog::OnDownloadTaskProgress)
    ON_MESSAGE(WM_DOWNLOAD_TASK_COMPLETE, &CMyDialog::OnDownloadTaskComplete)
END_MESSAGE_MAP()

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Windows API 멀티스레딩 | CreateThread·동기화 완벽 가이드
  • C++ 멀티스레딩 완벽 가이드 | thread·mutex·condition_variable
  • MFC 기초 | Microsoft Foundation Class 시작 가이드

이 글이 도움이 되셨나요? MFC 멀티스레딩을 마스터하는 데 도움이 되었기를 바랍니다!

다음 글에서는 MFC 소켓 네트워크 프로그래밍을 다루겠습니다. 🌐