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_guard나std::unique_lock과 함께 사용하는 것이 안전
- RAII 기반
RAII: 객체 생성 시 자원 획득, 소멸 시 자동 해제하는 C++ 프로그래밍 기법
std::lock_guard
std::lock_guard는 RAII() 원칙을 적용하여 mutex를 관리하는 객체로 단순하고 경량화 되어있음
std::lock_guard의 용도
- 고정된 범위 내에서 자동으로 mutex 잠금과 해제를 관리
std::lock_guard의 특징
- 자동 잠금과 해제
- 객체 생성 시: 객체가 생성될 때 지정한 mutex의
lock()메서드를 호출되어 잠금 - 긱체 소멸 시: 소멸(스코프를 벗어날 때) 자동으로
unlock()이 호출되어 잠금해제
- 객체 생성 시: 객체가 생성될 때 지정한 mutex의
- 예외 안전성 보장
- 예외가 발생하거나 함수가 조기 종료되더라도 소멸자가 호출되어 반드시 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.