Post

std::mutex 활용한 동시성 제어

C++ 표준 라이브러리를 활용한 동시성 제어방법 소개

Mutex (Mutual exclusion)

  • Mutex는 다중 스레드 환경에서 공유 자원에 대한 동시 접근을 제어하기 위한 도구
  • 여러 스레드가 동시에 같은 자원에 접근할 때 데이터 경쟁(race condition)을 방지
    • 공유 데이터 보호 (벡터, 맵 등)
    • 파일 I/O나 로그 기록
    • 생산자-소비자 패턴에서의 공유 큐 접근
    • 자원 초기화 및 소멸 관리

std::mutex

std::mutex<mutex>헤더에 정의되어 있으며, C++11 부터 표준 라이브러리에 포함

  • 한 번에 하나의 스레드만 임계 영역에 접근할 수 있도록 제한
    • 다른 스레드는 잠금이 해제될 때까지 대기(block)
  • 동일한 스레드가 두 번 이상 잠그는 것을 허용하지 않음(Non-Reentrant)
    • 재진입이 필요할 경우에는 std::recursive_mutex를 사용
  • lock(), unlock() 사용가능하나 예외상황 발생 시 데드락이 발생할 위험이 있음
    • RAII 기반 std::lock_guardstd::unique_lock과 함께 사용하는 것이 안전

RAII: 객체 생성 시 자원 획득, 소멸 시 자동 해제하는 C++ 프로그래밍 기법

std::lock_guard

std::lock_guard는 RAII() 원칙을 적용하여 mutex를 관리하는 객체로 단순하고 경량화 되어있음

std::lock_guard의 용도

  • 고정된 범위 내에서 자동으로 mutex 잠금과 해제를 관리

std::lock_guard의 특징

  • 자동 잠금과 해제
    • 객체 생성 시: 객체가 생성될 때 지정한 mutex의 lock() 메서드를 호출되어 잠금
    • 긱체 소멸 시: 소멸(스코프를 벗어날 때) 자동으로 unlock()이 호출되어 잠금해제
  • 예외 안전성 보장
    • 예외가 발생하거나 함수가 조기 종료되더라도 소멸자가 호출되어 반드시 mutex 해제
  • 유지보수성 향상
    • 직접 lock(), unlock()을 호출할 필요가 없기에, 코드의 가독성, 유지보수성 향상

std::unique_lock

std::unique_lock는 RAII() 원칙을 적용하여 mutex를 관리하는 객체로 더 유연한 기능 제공

std::unique_lock의 용도

  • std::lock_guard 보다 유연한 동기화 관리가 필요할 때 사용

std::unique_lock의 특징

  • 유연한 잠금 관리
    • 지연 잠금: 생성 시점에 바로 잠그지 않고, 나중에 필요할 때 lock()을 호출하여 잠글 수 있음
    • 잠금 시도: try_lock()을 사용하여, 즉시 잠금을 시도하고 성공 여부를 판단가능
    • 시간 제한 잠금: 일정 시간 동안 잠금을 시도하는 try_lock_for()try_lock_until() 사용가능
    • 수동 잠금/해제: 필요에 따라 lock()unlock()을 직접 호출가능
  • 이동 가능성
    • 함수 간에 소유권 이전 가능 → 다른 동기화 도구로는 불가능한 유연성을 제공

예시 (입출력 인터페이스)

  • 입출력 인터페이스(시리얼 포트, TCP 등)는 다중 스레드의 동시 접근 시 데이터 손실이나 충돌 위험이 있음
    • 이러한 위험을 방지하기 위해 mutex를 활용한 동기화 처리가 필요함
    • 실제 구현 함수에 내부 동기화가 이미 구현되어 있을 수 있으므로 확인 필요

가상 시리얼 포트를 이용한 예시 코드를 작성하였다. 아래 링크에서 전체를 확인할 수 있다.

https://github.com/grade-e/mutex-cpp-container

Class diagram

classDiagram
    class PortHandler {
        <<interface>>
        +bool open()
        +void close()
        +void write(const std::string &data)
        +std::string read()
    }

    class SerialPort {
        -std::string port_
        -int baudRate_
        -bool isOpen_
        +SerialPort(const std::string &port, int baudRate)
        +bool open()
        +void close()
        +void write(const std::string &data)
        +std::string read()
    }

    class SerialHandler {
        -std::unique_ptr<SerialPort> serial_
        -std::mutex serial_mutex_
        -std::string port_
        -int baudRate_
        +SerialHandler(const std::string &port, int baudRate)
        +bool open()
        +void close()
        +void write(const std::string &data)
        +std::string read()
    }

    PortHandler <|.. SerialHandler
    SerialHandler --> SerialPort : uses

PortHandler

인터페이스 역할을 하며, 포트 제어를 위한 기본 동작을 정의

1
2
3
4
5
6
7
class PortHandler {
   public:
    virtual bool open() = 0;
    virtual void close() = 0;
    virtual void write(const std::string& data) = 0;
    virtual std::string read() = 0;
};

SerialHandler

  • PortHandler 인터페이스를 구현
  • SerialPort 객체를 관리
  • 스레드 안전성을 위해 read(), write()std::mutex 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SerialHandler : public PortHandler {
   public:
    SerialHandler(const std::string& port, int baudRate)
        : port_(port), baudRate_(baudRate) {
        serial_ = std::make_unique<SerialPort>(port_, baudRate_);
    }

    bool open() override { return serial_->open(); }
    void close() override { serial_->close(); }
    void write(const std::string& data) override {
        std::lock_guard<std::mutex> lock(serial_mutex_);
        serial_->write(data);
    }
    std::string read() override {
        std::lock_guard<std::mutex> lock(serial_mutex_);
        return serial_->read();
    }

   private:
    std::unique_ptr<SerialPort> serial_;
    std::mutex serial_mutex_;
    std::string port_;
    int baudRate_;
};

SerialPort

시리얼 통신 기능을 콘솔 출력으로 시뮬레이션

main()

SerialHandler를 이용하여 다중 스레드 환경에서 데이터를 읽고 쓰는 예제 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
    SerialHandler serial_handler("/dev/ttyUSB0", 9600);
    if (!serial_handler.open()) {
        std::cerr << "Failed to open serial port." << std::endl;
        return 1;
    }
    std::thread writer1(&SerialHandler::write, &serial_handler, "Message 1");
    std::thread writer2(&SerialHandler::write, &serial_handler, "Message 2");
    std::thread reader(&SerialHandler::read, &serial_handler);

    writer1.join();
    writer2.join();
    reader.join();

    serial_handler.close();
    return 0;
}

출력 결과

실행함에 따라 아래와 같은 경우가 발생

결과 1: 호출 순서대로 나오는 경우

1
2
3
4
5
[Serial_] Open: /dev/ttyUSB0, baudrate: 9600
[Serial_] Write: Message 1
[Serial_] Write: Message 2
[Serial_] Read: Dummy Data
[Serial_] Close: /dev/ttyUSB0

결과 2: 호출 순서대로 나오지 않는 경우

1
2
3
4
5
[Serial_] Open: /dev/ttyUSB0, baudrate: 9600
[Serial_] Write: Message 2
[Serial_] Read: Dummy Data
[Serial_] Write: Message 1
[Serial_] Close: /dev/ttyUSB0

결과 정리

  • thread가 실행되는 순서는 운영체제의 스케줄링에 따라 달라질 수 있음
  • Mutex는 현재 thread가 완료될 때까지 다른 thread가 SerialPort에 접근 방지
  • 따라서, 위 결과는 각 thread가 서로 간섭 없이 안전하게 실행되고 있음을 의미
This post is licensed under CC BY 4.0 by the author.