[MFC, C++] CWinThread로 스레드 예제 구현

jckim22·2024년 7월 15일

[C++] STUDY

목록 보기
3/3

임계영역이란?

둘 이상의 스레드가 공유 자원에 접근할 때, 오직 한 스레드만 접근을 허용해야 하는 경우에 사용한다. 일반 동기화 객체와 달리 개별 프로세스의 유저(user) 메모리 영역에 존재하는 단순한 구조체이다. 따라서 다른 프로세스가 접근할 수 없으므로 한 프로세스에 속한 스레드 간 동기화에만 사용한다. 일반 동기화 객체보다 빠르고 효율적이다.

Critical Section Synchronization [User Mode] 

UserMode에서만 작동 해서 성능 측면에서 빠르다. 하지만 해당 방법은 하나의 프로세스, 하나의 프로세스에서의 Thread에서만 사용 가능 하다.

Mutex Synchronization [Kernel Mode]

Mutex라는 커널 객체를 이용해서 동기화를 하는 방법이다. 커널 객체를 사용하기 때문에 프로세스 사이에서의 동기화도 가능하다. 물론 쓰레드 사이에서의 동기화도 가능하다. 

Semaphore Synchronization [Kernel Mode] 

세마포어 역시 커널 객체를 이용한 동기화 방법이다.지정된 쓰레드 갯수 만큼 동시에 자원에 접근 할 수있도록 하는 것이다.멀트 쓰레드 프로그래밍을 할 때 쓰레드를 무제한 생성 같은 경우에 제한 할 수 있을 거같다.

이번 예제는 크리티컬 섹션을 사용해서 하나의 스레드만 임계영역에서 자원에 접근할 수 있게 함

CTread.h

#pragma once
#include <afxwin.h>
#include <afxmt.h>

class CThread : public CWinThread
{
    DECLARE_DYNCREATE(CThread)

protected:
    CThread();
    virtual ~CThread();

public:
    virtual BOOL InitInstance();
    virtual int ExitInstance();
    void SetThreadNum(int num);
    void SetMainWnd(CWnd* pWnd);

protected:
    DECLARE_MESSAGE_MAP()

private:
    int m_threadNum;
    CWnd* m_pMainWnd;  // Main window pointer for communication
    LONG m_nCounter;
    bool isCs;
    static const LONG g_nTarget;
    static CCriticalSection g_cs;
    static int g_nFlagCounter;
};
  • 메서드
    • CWinThread를 상속해서 CThread를 구현했다.
    • InitInstance: 쓰레드가 만들어지는 과정에서 로직을 구현한다.
    • ExitInstance: 쓰레드를 종료하는 과정
    • SetThreadNum: 현재 쓰레드의 NumberID를 결정한다
    • SetMainWnd: 현재 부모 다이얼로그를 설정한다.
  • 전역, 멤버 변수
    • m_threadNum, m_pMainWnd: 현재 쓰레드 NumberId와 부모 다이얼로그 포인터 변수
    • m_nCounter: member_numberCounter이다, 각 쓰레드의 카운트 넘버이다.
    • isCs: 현재 임계영역에 들어왔는지에 대한 flag이다.
    • g_nTarget: 공통된 목표(FLAG) 이다.
    • g_cs: 크리티컬 섹션을 다룰 수 있는 객체이다.
    • g_nFlagCounter: Flag에 도달한 스레드를 카운터 해주는 전역 변수이다.

CThread.cpp

#include "pch.h"
#include "CThread.h"

IMPLEMENT_DYNCREATE(CThread, CWinThread)

CThread::CThread() : m_threadNum(0), m_pMainWnd(nullptr), m_nCounter(0)
{
}

CThread::~CThread()
{
}

BOOL CThread::InitInstance()
{
    while (true)
    {
        ++m_nCounter;
        
        if (!isCs && m_nCounter > (g_nTarget - 300))
        {
            CString msg;
            msg.Format(_T("%d번 쓰레드: 임계영역 진입"), m_threadNum);
            if (m_pMainWnd)
            {
                m_pMainWnd->PostMessage(WM_USER + 1, (WPARAM)new CString(msg));
            }
            isCs = TRUE;
            g_cs.Lock();
        }
        
        if (m_nCounter > g_nTarget)
        {
            CString msg;
            msg.Format(_T("%d번 쓰레드: 목표치 도달 (완료 스레드: %d/3)"), m_threadNum, ++g_nFlagCounter);
            if (m_pMainWnd)
            {
                m_pMainWnd->PostMessage(WM_USER + 1, (WPARAM)new CString(msg));
            }
            g_cs.Unlock();
            if (g_nFlagCounter == 3) {
                CString msg;
                msg.Format(_T("완료"));
                m_pMainWnd->PostMessage(WM_USER + 1, (WPARAM)new CString(msg));
            }
            break;
        }
        
        if (m_pMainWnd)
        {
            CString* pStr = new CString;
            pStr->Format(_T("Thread %d: %ld"), m_threadNum, m_nCounter);
            m_pMainWnd->PostMessage(WM_USER + 1, (WPARAM)pStr);
        }

        Sleep(1);
    }
    return TRUE;
}

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

void CThread::SetThreadNum(int num)
{
    m_threadNum = num;
}

void CThread::SetMainWnd(CWnd* pWnd)
{
    m_pMainWnd = pWnd;
}

const LONG CThread::g_nTarget = 500;
CCriticalSection CThread::g_cs;
bool isCs = false;
int CThread::g_nFlagCounter = 0;

BEGIN_MESSAGE_MAP(CThread, CWinThread)
END_MESSAGE_MAP()
  • 스레드 생성이 시작되면 while 문으로 m_nCounter를 1씩 증가한다.
  • if (!isCs && m_nCounter >= (g_nTarget - 500))
    • 현재 임계영역에 들어왔지 않고 카운트가 목표치보다 500적으면 조건에 들어온다.
    • 임계영역에 진입했다는 메시지를 메인 윈도우에 보내고 임계영역에 들어왔다는 체크를 한다.
    • g_cs로 공유자원에 대한 Lock을 건다.
  • if (m_nCounter >= g_nTarget)
    • 목표치에 도달하면 그에 맞는 메시지를 보낸다.
    • 공유 자원에 대한 lock을 풀어준다.
  • if (m_pMainWnd)
    • 반복 때 마다 현재 스레드의 카운트 정보를 보내준다.
  • Sleep(1)
    • 변화를 확인하기 위해 매 반복마다 1의 Sleep을 걸어준다.

CDThreadBox.h

class CDThreadBox : public CDialogEx
{
    DECLARE_DYNAMIC(CDThreadBox)

public:
    CDThreadBox(CWnd* pParent = nullptr);   // 표준 생성자입니다.
    virtual ~CDThreadBox();

#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_DIALOG_THREAD };
#endif

protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 지원입니다.

    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnStnClickedStatic1();
    afx_msg void OnStnClickedStatic2();
    afx_msg void OnStnClickedStatic3();
    afx_msg LRESULT OnUpdateCounter(WPARAM wParam, LPARAM lParam);
    afx_msg LRESULT OnThreadMessage(WPARAM wParam, LPARAM lParam);

private:
    CStatic m_thread_box1;
    CStatic m_thread_box2;
    CStatic m_thread_box3;
    CStatic m_result_box;
};
  • 원래는 OnThreadMessage로 각 스레드 메시지들을 업데이트하고 OnUpdateCounter로 전체 상황을 업데이트하려고 했으나 if문으로 OnUpdateCounter에서 다 관리하게 되었다.

CDThreadBox.cpp

#include "pch.h"
#include "ThreadSample.h"
#include "afxdialogex.h"
#include "CDThreadBox.h"
#include "CThread.h"

IMPLEMENT_DYNAMIC(CDThreadBox, CDialogEx)

CDThreadBox::CDThreadBox(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_DIALOG_THREAD, pParent)
{
}

CDThreadBox::~CDThreadBox()
{
}

void CDThreadBox::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_STATIC_1, m_thread_box1);
    DDX_Control(pDX, IDC_STATIC_2, m_thread_box2);
    DDX_Control(pDX, IDC_STATIC_3, m_thread_box3);
    DDX_Control(pDX, IDC_STATIC_4, m_result_box);
}

BEGIN_MESSAGE_MAP(CDThreadBox, CDialogEx)
    ON_STN_CLICKED(IDC_STATIC_1, &CDThreadBox::OnStnClickedStatic1)
    ON_STN_CLICKED(IDC_STATIC_2, &CDThreadBox::OnStnClickedStatic2)
    ON_STN_CLICKED(IDC_STATIC_3, &CDThreadBox::OnStnClickedStatic3)    
    ON_MESSAGE(WM_USER + 1, &CDThreadBox::OnUpdateCounter)
END_MESSAGE_MAP()

void CDThreadBox::OnStnClickedStatic1()
{
    CThread* pThread = (CThread*)AfxBeginThread(RUNTIME_CLASS(CThread));
    pThread->SetThreadNum(1);
    pThread->SetMainWnd(this);
}

void CDThreadBox::OnStnClickedStatic2()
{
    CThread* pThread = (CThread*)AfxBeginThread(RUNTIME_CLASS(CThread));
    pThread->SetThreadNum(2);
    pThread->SetMainWnd(this);
}

void CDThreadBox::OnStnClickedStatic3()
{
    CThread* pThread = (CThread*)AfxBeginThread(RUNTIME_CLASS(CThread));
    pThread->SetThreadNum(3);
    pThread->SetMainWnd(this);
}

LRESULT CDThreadBox::OnThreadMessage(WPARAM wParam, LPARAM lParam)
{
    //CString* pMsg = (CString*)wParam;
    //CString currentText;
    //m_thread_box1.GetWindowText(currentText);
    //currentText += _T("\r\n") + *pMsg;
    //m_thread_box1.SetWindowText(currentText);
    //delete pMsg;
    return 0;
}

LRESULT CDThreadBox::OnUpdateCounter(WPARAM wParam, LPARAM lParam)
{
    CString* pStr = (CString*)wParam;
    
    if (pStr->Find(_T("Thread 1:")) == -1 &&
        pStr->Find(_T("Thread 2:")) == -1 &&
        pStr->Find(_T("Thread 3:")) == -1)
    {        
        CString currentText;
        m_result_box.GetWindowText(currentText);
        currentText += _T("\r\n") + *pStr;
        m_result_box.SetWindowText(currentText);
    }
    else
    {        
        if (pStr->Find(_T("Thread 1:")) != -1)
        {
            m_thread_box1.SetWindowText(*pStr);
        }
        else if (pStr->Find(_T("Thread 2:")) != -1)
        {
            m_thread_box2.SetWindowText(*pStr);
        }
        else if (pStr->Find(_T("Thread 3:")) != -1)
        {
            m_thread_box3.SetWindowText(*pStr);
        }
    }

    delete pStr;
    return 0;
}

  • 각 이벤트를 메시지 맵에 매핑 해준다.
    • ON_MESSAGE(WM_USER + 1, &CDThreadBox::OnUpdateCounter)
      • 스레드에서 직접 WM_USER + 1를 보내서 이벤트를 발생시킨다.
  • OnStnClickedStatic1N
    • 각 컨트롤 클릭 시, 스레드를 생성하고 MainWnd를 현재 메인 윈도우(다이얼로그)로 설정해준다.
  • OnUpdateCounter
    • WM_USER+1로 임의로 설정한 메시지가 오면 이벤트 핸들러가 발동 된다.
    • 문자열로 구분해서 동작을 결정한다.
    • "Thread N"이 없다면 전체 로그 박스에 Text를 누적해서 업데이트 한다.

실습

  • TargetN을 500으로 설정한다.
  • 그럼 200부터는 임계영역에 들어오게 된다.
  • 먼저 한 스레드가 목표치에 도달하면 lock이 풀리고 두번째 대기한 스레드가 임계영역 동작을 할 것이다.
  • 반복하고 3개의 스레드가 전부 FLAG를 얻게 되면 종료 된다.

  • 200까지는 동시에 동작한다.
  • 3번이 임계영역에서 동작하고 있기 때문에 1,2번은 동작할 수 없다.

  • 3번이 끝나고 2번 1번 순으로 차례로 동작을 완료한다.

스레드를 비동기적으로 동시에 시작하기

각각 클릭이 아닌 스레드 3개를 동시에 시작할 방법은 비동기적으로 한번에 이벤트를 발생시켜야 가능할 것이다.

그럼 PostMessage를 사용해야하고 메시지를 직접 보내 핸들러를 작동시켜야만 한다.

사전작업

  • 리소스 뷰에서 3개의 스태틱컨트롤을 컨트롤할 버튼을 하나 만든다.
  • 아래처럼 3개의 메서드를 메시지를 통해 실행해줄 메서드를 매핑해서 만든다.
LRESULT CDThreadBox::OnStaticClicked1(WPARAM wParam, LPARAM lParam)
{
    OnStnClickedStatic1();
    return 0;
}

LRESULT CDThreadBox::OnStaticClicked2(WPARAM wParam, LPARAM lParam)
{
    OnStnClickedStatic2();
    return 0;
}

LRESULT CDThreadBox::OnStaticClicked3(WPARAM wParam, LPARAM lParam)
{
    OnStnClickedStatic3();
    return 0;
}
  • 아래처럼 메시지 맵에 임의의 ID로 핸들러와 메시지를 매핑하여 등록한다.
    ON_MESSAGE(WM_USER_STATIC_CLICKED1, &CDThreadBox::OnStaticClicked1)
    ON_MESSAGE(WM_USER_STATIC_CLICKED2, &CDThreadBox::OnStaticClicked2)
    ON_MESSAGE(WM_USER_STATIC_CLICKED3, &CDThreadBox::OnStaticClicked3)

SendMessage

  • 윈도우 프로시저를 직접 호출하며, 프로시저가 메시지를 처리할 때 까지 반환하지 않는다.

    동기적이고 순차적으로 실행된다.

PostMessage

  • 메시지 큐에 메시지가 삽입되며, 윈도우 프로시저에서 메시지를 처리한다.
  • 해당 메시지가 언제 처리될 지 예측이 어렵다.

    비동기적이면서 비순차적이다.

SendMessage 실습

void CDThreadBox::OnStnClickedStatic5()
{
    SendMessage(WM_USER_STATIC_CLICKED1);
    Sleep(1);
    SendMessage(WM_USER_STATIC_CLICKED2);
    Sleep(1);
    SendMessage(WM_USER_STATIC_CLICKED3);
}
  • 먼저 SendMessage로 테스트를 해보았다.
  • Sleep으로 결과를 조금 더 명확히 확인한다.

  • 10번 정도 실행해봤을 때 10번 다 같은 순차적인 결과가 나왔다.

PostMessage 실습

void CDThreadBox::OnStnClickedStatic5()
{
    PostMessage(WM_USER_STATIC_CLICKED1);
    Sleep(1);
    PostMessage(WM_USER_STATIC_CLICKED2);
    Sleep(1);
    PostMessage(WM_USER_STATIC_CLICKED3);
}

  • 매 실행마다 불규칙적인 결과가 나왔다.
profile
개발/보안

1개의 댓글

comment-user-thumbnail
2025년 2월 12일

안녕하세요
MFC 동기화 관련해서 공부중이였는데 도움이 많이 됐습니다. 감사합니다.
실례가 안된다면 실습 예제 솔루션 파일을 받아볼 수 있을까요?

답글 달기