std::mutex
개요
std::mutex
는 "Mutual Exclusion"의 약자로, 여러 스레드가 동시에 공유 자원(shared resource)에 접근하는 것을 막기 위한 동기화 메커니즘입니다. 코드의 특정 영역을 임계 영역(critical section)으로 지정하여, 한 번에 하나의 스레드만 해당 영역의 코드를 실행할 수 있도록 보장합니다. 이를 통해 데이터 경쟁(data race)을 방지하고 프로그램의 안정성을 높일 수 있습니다.
C++ 버전별 주요 키워드 도입 시기
- C++11:
std::mutex
,std::lock_guard
,std::unique_lock
등 기본적인 뮤텍스 기능이 도입되었습니다. - C++14:
std::shared_timed_mutex
가 추가되어 읽기-쓰기 잠금(read-write lock)을 구현할 수 있게 되었습니다. - C++17:
std::scoped_lock
이 추가되어 여러 뮤텍스를 데드락(deadlock) 발생 위험 없이 안전하게 잠글 수 있는 기능을 제공합니다.
내용 설명
경쟁 상태 (Race Condition)
여러 스레드가 동일한 메모리 위치(공유 자원)에 동시에 접근하여 읽거나 쓰려고 할 때 경쟁 상태가 발생할 수 있습니다. 특히 하나 이상의 스레드가 데이터를 수정하는 경우, 연산이 원자적(atomic)으로 처리되지 않아 예기치 않은 결과가 발생할 수 있습니다.
예를 들어 worker1
함수에서 ++global_counter
연산은 단일 기계어 명령이 아닐 수 있습니다. 일반적으로 아래와 같은 3단계로 나뉩니다.
- 메모리에서
global_counter
값을 레지스터로 읽어온다. (Read) - 레지스터의 값을 1 증가시킨다. (Modify)
- 레지스터의 값을 다시 메모리에 쓴다. (Write)
두 개의 스레드가 이 연산을 거의 동시에 수행하면, 한 스레드의 변경 사항이 다른 스레드에 의해 덮어씌워져 카운트가 누락될 수 있습니다. 그 결과 global_counter
의 최종 값은 NUM_THREADS * NUM_INCREMENTS
보다 작아지게 됩니다.
std::mutex
와 std::lock_guard
std::mutex
는 이러한 경쟁 상태를 해결하기 위해 사용됩니다. 공유 자원에 접근하는 코드 블록(임계 영역)의 시작과 끝에서 lock()
과 unlock()
을 호출하여 해당 영역을 보호합니다.
하지만 unlock()
을 직접 호출하는 방식은 프로그래머가 호출을 잊거나, 코드 중간에 예외가 발생했을 때 잠금이 해제되지 않는 문제를 야기할 수 있습니다. 이러한 문제를 해결하기 위해 C++에서는 RAII(Resource Acquisition Is Initialization) 패턴을 활용하는 std::lock_guard
를 제공합니다.
std::lock_guard
는 생성자에서 전달받은 mutex
를 자동으로 잠그고, 객체가 소멸될 때(스코프를 벗어날 때) 소멸자에서 자동으로 잠금을 해제합니다. 이를 통해 코드가 더 안전하고 간결해집니다.
worker2
함수는 std::lock_guard
를 사용하여 global_counter
를 안전하게 업데이트하는 방법을 보여줍니다. 각 스레드는 먼저 로컬 변수 local_counter
를 증가시킨 뒤, 뮤텍스로 보호된 임계 영역에 진입하여 global_counter
에 한 번만 더해줍니다. 이는 뮤텍스 잠금 시간을 최소화하여 성능을 향상시키는 좋은 방법입니다.
예제 코드
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <mutex>
#include <numeric>
#include <thread>
namespace
{
constexpr auto NUM_THREADS = std::uint32_t{20U};
constexpr auto NUM_INCREMENTS = std::uint32_t{100'000U};
}; // namespace
auto global_counter = std::int32_t{0};
// 경쟁 상태를 유발하는 워커 함수 (사용되지 않음)
void worker1(const std::int32_t input, std::int32_t &output)
{
output = input * 2;
for (std::uint32_t i = 0; i < NUM_INCREMENTS; ++i)
{
++global_counter; // 데이터 경쟁 발생 지점
}
}
auto mutex = std::mutex{};
// std::lock_guard로 임계 영역을 보호하는 워커 함수
void worker2(const std::int32_t input, std::int32_t &output)
{
output = input * 2;
auto local_counter = std::uint32_t{0U};
for (std::uint32_t i = 0; i < NUM_INCREMENTS; ++i)
{
++local_counter;
}
// lock_guard가 생성되면서 mutex가 잠김
auto guard = std::lock_guard<std::mutex>{mutex};
global_counter += local_counter;
// guard가 스코프를 벗어나면서 소멸되고, mutex가 자동으로 해제됨
}
int main()
{
auto inputs = std::array<std::int32_t, NUM_THREADS>{};
std::iota(inputs.begin(), inputs.end(), 0);
auto outputs = std::array<std::int32_t, NUM_THREADS>{};
std::fill(outputs.begin(), outputs.end(), 0);
std::array<std::thread, NUM_THREADS> threads;
for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
{
threads[i] = std::thread(worker2, inputs[i], std::ref(outputs[i]));
}
for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
{
threads[i].join();
}
for (std::uint32_t i = 0; i < NUM_THREADS; ++i)
{
std::cout << "Outputs[" << i << "] = " << outputs[i] << '\n';
}
std::cout << "Global counter = " << global_counter << '\n';
assert(global_counter == NUM_THREADS * NUM_INCREMENTS);
return 0;
}
실행 결과
Outputs[0] = 0
Outputs[1] = 2
Outputs[2] = 4
Outputs[3] = 6
Outputs[4] = 8
Outputs[5] = 10
Outputs[6] = 12
Outputs[7] = 14
Outputs[8] = 16
Outputs[9] = 18
Outputs[10] = 20
Outputs[11] = 22
Outputs[12] = 24
Outputs[13] = 26
Outputs[14] = 28
Outputs[15] = 30
Outputs[16] = 32
Outputs[17] = 34
Outputs[18] = 36
Outputs[19] = 38
Global counter = 2000000
std::mutex
를 사용하여 global_counter
에 대한 접근을 동기화했기 때문에, 최종 결과는 정확히 20 * 100,000 = 2,000,000
이 되며 assert
문이 성공적으로 통과합니다.
활용팁
- 잠금 범위 최소화: 뮤텍스는 성능에 영향을 줄 수 있으므로, 임계 영역의 범위를 최대한 작게 유지하는 것이 중요합니다. 예제 코드의
worker2
처럼, 동기화가 필요 없는 계산은 잠금 영역 밖에서 수행하고 꼭 필요한 부분만 잠그는 것이 좋습니다. std::lock_guard
사용 생활화:unlock()
을 직접 호출하기보다std::lock_guard
를 사용하여 잠금 해제를 자동화하면 실수를 줄이고 예외 안전성을 높일 수 있습니다.- 데드락 주의: 두 개 이상의 뮤텍스를 사용할 때는 데드락이 발생할 수 있습니다. 항상 같은 순서로 뮤텍스를 잠그거나, C++17 이상이라면
std::scoped_lock
을 사용하여 여러 뮤텍스를 한 번에 안전하게 잠그는 것을 권장합니다. std::mutex
는 복사/이동 불가:std::mutex
객체는 복사나 이동이 불가능합니다. 클래스 멤버로 사용할 경우 포인터나 참조로 관리해야 할 수 있습니다.
'개발 > C++ (98,03,11,14,17,20,23)' 카테고리의 다른 글
Modern C++ : std::shared_timed_mutex (14) (0) | 2025.10.19 |
---|---|
Modern C++ : std::thread (11) (0) | 2025.10.17 |
Modern C++ : std::exception (98, 11, 17) (0) | 2025.10.16 |
Modern C++ : std::weak_ptr (11) (1) | 2025.10.15 |
Modern C++ : std::shared_ptr (11, 17) (0) | 2025.10.14 |
Modern C++ : std::unique_ptr (11, 14) (0) | 2025.10.13 |
Modern C++ : std::format (20) (0) | 2025.10.12 |
Modern C++ : std::ranges (20) (0) | 2025.10.11 |