ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 윈도우 유저 모드 동기화 CRITICAL_SECTION
    개발/C++ 2019. 11. 4. 03:04

    동기화 방법은 크게 유저 모드와 커널 모드가 있다. 커널 모드 동기화의 기능이 굳이 필요한 상황이 아니라면 유저 모드 동기화가 성능이 좋다. 응용 프로그램은 기본적으로 유저 모드에서 동작하기 때문에, 커널 모드 동기화를 사용하면 동기화할 때마다 커널에 접근해야 하므로 성능에 불이익이 생길 수밖에 없다.

     

    유저 모드 동기화 방법은 주로 CRITICAL_SECTION(이하 CS)이 사용된다. 단순하게 변수의 정수값을 증감할 때는 Interlocked 계열 함수를 이용하면 되며 임계 영역을 동기화해야 하는 경우에는 CS로 동기화한다. CS로 동기화를 진행하기 위해서는 일단 CS 객체를 만들어야 한다. 초기화는 매개변수로 CS 객체의 포인터를 넣는 InitializeCriticalSection 함수를 사용하고 아예 사용을 끝낼 때는 역시 CS 객체의 포인터 하나를 인자로 받는DeleteriticalSection 함수를 호출한다. 임계 영역이 시작하는 곳에 EnterCriticalSection 함수를 호출하고 끝나는 곳에 LeaveCriticalSection 함수를 넣는다. 두 함수 모두 인자는 하나만 필요하며 CS 객체의 포인터 변수다.

     

    소유권을 가지고 있는 스레드는 임계 영역이 끝나면 반드시 소유권을 반납해야 동기화로 인한 문제가 생기지 않는다. 하지만 사람은 실수하기 마련이므로 규칙을 정했다고 해서 지키는 것을 언제나 보장하기 어렵다. 간단하게 사용할 수 있는 설계가 필요한 이유다. CS는 사용 전에 초기화가 필요하고 사용이 완전히 끝난 뒤에는 해당 리소스를 반환해야 하며, 임계 영역의 진입과 퇴장 때의 처리가 필수다. 객체의 생명 주기에 따라 자동으로 호출되는 함수인 생성자와 소멸자를 활용해 사용하기 편한 동기화 객체를 만들 수 있다.

     

    크게 두 가지의 처리(객체 초기화/해제, 임계영역 진입/퇴장)를 해야 한다는 의미는 두 번의 생성자/소멸자 호출이 필요하다는 뜻이다. 이 문제는 클래스 안에 클래스를 하나 더 만들어 간단하게 해결할 수 있다. 동기화 클래스는 CS 객체의 임시 복사를 최소화하기 위해 참조를 활용할 것이다. CS 객체의 크기는 무려 40바이트다. 참조 객체는 메모리를 차지 안 할 수도 있지만 대부분 포인터의 크기만큼의 메모리만 사용하는 까닭에 적극 사용해 낭비를 줄이자. 설명이 길었다. 코드로 확인하자. 다음은 헤더.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #pragma once
    #include "stdafx.h"
     
    class Monitor
    {
    public:
        class Owner
        {
        public:
            Owner(Monitor& object);
            ~Owner();
     
        private:
            Monitor& sync_object_;
            Owner(const Owner& rhs);
            Owner& operator=(const Owner& rhs);
        };
     
        Monitor();
        ~Monitor();
     
        void Enter();
        void Leave();
     
    private:
        CRITICAL_SECTION sync_object_;
        Monitor(const Monitor& rhs);
        Monitor& operator=(const Monitor& rhs);
    };
     

    이어서 소스파일.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include "stdafx.h"
    #include "Monitor.h"
     
    Monitor::Monitor()
    {
        InitializeCriticalSection(&sync_object_);
    }
     
    Monitor::~Monitor()
    {
        DeleteCriticalSection(&sync_object_);
    }
     
    void Monitor::Enter()
    {
        EnterCriticalSection(&sync_object_);
    }
     
    void Monitor::Leave()
    {
        LeaveCriticalSection(&sync_object_);
    }
     
    Monitor::Owner::Owner(Monitor& object):sync_object_(object)
    {
        sync_object_.Enter();
    }
     
    Monitor::Owner::~Owner()
    {
        sync_object_.Leave();
    }
     

     

    임계 영역 앞에서 Monitor 객체인 sync_object_(멤버 변수)를 Monitor::Owner lock(sync_object_) 이런 방법으로 사용하면 된다. Monitor 객체가 만들어지는 순간 CS 객체가 생성되고 Monitor 생성자를 통해 초기화가 진행된다. lock 지역 변수가 생성될 때 Owner 생성자를 통해 Enter 함수가 호출되고 lock 지역 변수가 범위를 벗어나 스택에서 제거되면 소멸자가 호출되면서 Leave 함수가 호출된다. 멤버 변수인 sync_object_가 메모리에서 사라질 때는 Monitor 소멸자가 호출될 테니 CS 객체의 리소스가 반환될 것이다. 이 동기화 클래스 덕분에 동기화를 진행할 때 지켜야 하는 규칙에 관해서만큼은 신경 쓸 거리가 줄어들게 되었다.

    댓글

Designed by Tistory.