멀티 코어 환경에서 동기화 자원의 접근을 보장하는 방법은 여러가지가 있습니다. 대표적인 방법으로는 os 가 제공하는 동기화 방법인 mutex와 cpu의 하드웨어 명령어를 직접사용하는 atomic 방법이 있습니다. 추가로, 락이 풀릴 때까지 무한 루프를 돌며(cpu를 계속 점유하며) 대기하는 방식인 스핀락이 있습니다. 오늘은 이 세가지 방식에 대해서 알아보겠습니다.
1. mutex
mutex 방식은 스레드가 자원을 사용하려고 할 때 lock을 걸어 (sleep) 대기하여 동기화 자원의 접근을 보장하는 방법입니다. 이는 운영체제 커널이 관리하는 락 메커니즘을 사용하며, 락을 잡지 못한 스레드는 대기 상태(sleep)로 들어가면서 다른 스레드가 CPU를 쓸 수 있도록 해줍니다. 그래서 락이 풀릴 때까지 해당 스레드는 블로킹되어 작업을 멈춥니다.
이 과정에서 컨텍스트 스위치, 커널 - 유저 모드 전환 비용 등이 발생하기 때문에 mutex를 사용하면 성능 저하가 발생될 수 있습니다.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void worker_mutex(int id) {
for (int i = 0; i < 5; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 락 획득
++shared_data;
std::cout << "Thread " << id << " incremented shared_data to " << shared_data << "\n";
}
}
int main() {
std::thread t1(worker_mutex, 1);
std::thread t2(worker_mutex, 2);
t1.join();
t2.join();
std::cout << "Final shared_data: " << shared_data << "\n";
return 0;
}
2. atomic
atomic 방식은 락을 사용하지 않는 lock-free 방식으로, os 커널 개입없이 CPU의 원자적 명령어 (atomic instructions)를 이용해 동기화된 자원 접근을 보장합니다. atomic 방식은 값 변경 시도가 충돌하면 짧은 박복 재시도를 통해 안전하게 자원을 갱신하며, 단순한 연산에서는 대기 시간이 매우 짧아 높은 성능을 보입니다. 따라서 atomic 연산은 주로 단일 변수에 대해 여러 스레드가 동시에 읽고 쓰는 상황에서 안전하게 동작하도록 하기 위해 사용합니다.
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> shared_data{0};
void worker_atomic(int id) {
for (int i = 0; i < 5; ++i) {
shared_data++;
std::cout << "Thread " << id << " incremented shared_data from " << old_val << " to " << old_val + 1 << "\n";
}
}
int main() {
std::thread t1(worker_atomic, 1);
std::thread t2(worker_atomic, 2);
t1.join();
t2.join();
std::cout << "Final shared_data: " << shared_data.load() << "\n";
return 0;
}
참고로 쓰기와 읽기 순서 및 캐시 동기화를 보장하기 위해서는 atomic의 release + acquire memory ordering 옵션을 사용해야 합니다.
void writer() {
data = 42;
ready.store(true, std::memory_order_release); // 순서 보장
}
void reader() {
while (!ready.load(std::memory_order_acquire)) {}
std::cout << "Reader sees data = " << data << "\n"; // 항상 42 출력됨
}
3. spin lock
spinlock 방식은 CPU의 원자적 명령어(atomic operation)을 사용해 구현하는 lock 방식으로, 구조체나 여러 변수 같은 복합 데이터에 대해 임계 영역 내 원자성을 보장할 때 사용됩니다. spinlock은 락을 획득하지 못하면 자원의 접근 권한을 얻기 위해 대기(sleeping)하는 것이 아니라 스레드가 CPU를 계속 점유한 채 락 상태를 반복해서 확인(busy waiting)하기 때문에 CPU 낭비가 발생합니다 따라서 락을 매우 짧은 시간만 잡는 임계영역에서 주로 사용됩니다.
스핀락 방식은 컨텍스트 스위치 등의 비용없이 바로 락을 재시도 하기 때문에 짧은 임계 영역에서는 뮤텍스보다 훨씬 빠르게 동작합니다
스핀락은 주로 std::atomic_flag를 사용해 구현되며, std::atomic<bool>과 비슷하지만 lock-free를 보장하고 인터페이스가 단순화되어 있어 스핀락 구현에 더 적합합니다. atomic_flag는 가장 기본적인 atomic 타입으로, 불필요한 연산 없이 최소한의 기능(test_and_set, clear)만 제공하여 매우 효율적으로 동작합니다.
#include <iostream>
#include <thread>
#include <atomic>
// 스핀락 클래스
class SpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) { //flag를 true로 바꾸고 false를 return하여 루프 탈출
// busy-wait
}
}
void unlock() {
flag.clear(std::memory_order_release); //flag = false
}
};
// 공유 자원: 두 변수로 구성된 구조체
struct SharedData {
int x = 0;
int y = 0;
};
SpinLock spinlock;
SharedData shared;
// 스레드 함수
void worker_spinlock(int id) {
for (int i = 0; i < 5; ++i) {
spinlock.lock();
// 두 변수에 동시에 연산 (원자적으로 보장해야 함)
shared.x += 1;
shared.y += 2;
std::cout << "Thread " << id << " updated x = " << shared.x
<< ", y = " << shared.y << "\n";
spinlock.unlock();
}
}
int main() {
std::thread t1(worker_spinlock, 1);
std::thread t2(worker_spinlock, 2);
t1.join();
t2.join();
std::cout << "Final shared data: x = " << shared.x << ", y = " << shared.y << "\n";
return 0;
}
이처럼 스핀 락을 사용하면 atomic으로 각각 보호했을 때와 달리 x, y 가 하나의 블록으로 동기화 되어 원자적 보호가 가능합니다.
비교) atomic (Lock-free) spinlock (Busy-wait Lock) mutex (Blocking Lock)
락 존재 | 없음 → 락 프리 | 있음 (소프트웨어 락) | 있음 (커널 락) |
대기 방식 | 없음 (실패 시 CAS 재시도) | 반복 루프 (CPU 계속 점유) | 스레드 sleep (커널이 스케줄링) |
락 프리 | lock-free 보장 (일부 타입 제외) | 구현에 따라 다름, 보통 lock-based | 락 기반, 항상 blocking |
스레드 블록 | 안 됨 | 안 됨 | 됨 (sleep) |
실패 시 동작 | 바로 재시도 (CAS) | 계속 loop 돌며 대기 (busy-wait) | 커널에 진입 후 sleep |
CPU 효율 | 매우 효율적 | 효율 낮음 (짧은 경우만 빠름) | 효율적 (긴 작업에 적합) |
용도 | 단일 변수 갱신 | 구조체 등 복합 연산 | 범용 락 |
락 구현 방식 | 하드웨어 CAS | atomic_flag 이용한 소프트웨어 락 | 커널 오브젝트 (e.g. pthread_mutex) |
참고 링크)
atomic flag
std::atomic_flag - cppreference.com
std::atomic_flag - cppreference.com
class atomic_flag; (since C++11) std::atomic_flag is an atomic boolean type. Unlike all specializations of std::atomic, it is guaranteed to be lock-free. Unlike std::atomic , std::atomic_flag does not provide load or store operations. [edit] Member functio
www.cppreference.com
atomic
std::atomic - cppreference.com
std::atomic - cppreference.com
template< class T > struct atomic; (1) (since C++11) template< class U > struct atomic ; (2) (since C++11) (3) (since C++20) (4) (since C++20) #define _Atomic(T) /* see below */ (5) (since C++23) Each instantiation and full specialization of the std::atomi
www.cppreference.com
memory order
std::memory_order - cppreference.com
std::memory_order - cppreference.com
enum memory_order { memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst }; (since C++11) (until C++20) enum class memory_order : /* unspecifi
en.cppreference.com
'C++' 카테고리의 다른 글
read/write과 sendfile 방식 비교 (0) | 2025.06.20 |
---|---|
11. [C++] 파티션 메모리 복사 (dd 명령어, 파일 입출력) (1) | 2025.06.03 |
10. [C++] 가중치가 없는 그래프에서 사이클 탐지 및 지름 구하기 (0) | 2025.06.03 |
09. [C++] 벡터와 리스트에서의 erase/remove 사용법 (0) | 2025.06.01 |
08. C++_string class와 관련 기능 (0) | 2024.06.16 |