2024. 11. 4. 23:23 ㆍ개발 이야기/아키텍처
멀티스레딩 환경에서의 데이터 공유는 매우 도전적인 문제 중 하나입니다. 락을 사용해 공유 자원을 보호하는 것은 가장 일반적인 방법이지만, 락을 사용하면 성능 저하와 데드락 같은 문제에 부딪힐 수 있습니다. 이러한 문제를 해결하기 위해 등장한 개념 중 하나가 바로 락 프리(Lock-Free) 자료구조입니다. 이번 포스팅에서는 락 프리 큐(Lock-Free Queue)의 개념을 이해하고, 코드 예제를 통해 어떻게 동작하는지 알아보겠습니다.
락 프리 자료구조란?
락 프리 자료구조는 스레드 간의 동시 접근이 발생해도 락을 사용하지 않고 안전하게 데이터를 관리할 수 있는 구조를 말합니다. 락 프리 자료구조의 주요 목표는 성능 향상입니다. 이를 위해 락 대신 원자적 연산(Atomic Operation)을 사용하여 데이터의 일관성을 유지합니다.
락 프리 큐는 대기 상태(waiting)를 줄이고, 여러 스레드가 동시에 데이터 구조에 접근하더라도 서로 차단(blocking)하지 않는 특징을 가집니다. 이를 통해 높은 동시성을 요구하는 환경에서 성능을 크게 향상시킬 수 있습니다.
락 프리 큐의 기본 개념
락 프리 큐는 주로 CAS(Compare-And-Swap) 연산을 사용하여 구현됩니다. CAS는 특정 메모리 위치의 값을 예상한 값으로 비교하고, 예상이 맞다면 새로운 값으로 교체하는 연산입니다. 이를 통해 데이터의 상태를 안전하게 갱신할 수 있으며, 여러 스레드가 동시에 접근하더라도 데이터의 일관성을 유지할 수 있습니다.
락 프리 큐를 구현하는 데 많이 사용되는 알고리즘은 Michael & Scott 큐(Michael and Scott Queue)입니다. 이 큐는 단일 연결 리스트로 구현되며, 두 개의 포인터(헤드와 테일)를 유지하면서 FIFO(First-In-First-Out) 특성을 보장합니다.
락 프리 큐 예제 코드
아래는 Python에서 락 프리 큐를 구현한 간단한 예제입니다. Python은 기본적으로 락 프리 자료구조를 지원하지 않지만, concurrent.futures 모듈이나 multiprocessing 모듈을 활용하거나 원자적 연산을 직접 구현하여 유사하게 동작하게 할 수 있습니다.
import threading
from collections import deque
import ctypes
class LockFreeQueue:
def __init__(self):
self.queue = deque()
self.lock = threading.Lock()
def enqueue(self, value):
while True:
with self.lock:
try:
self.queue.append(value)
print(f"Enqueued: {value}")
return
except Exception as e:
print(f"Enqueue failed: {e}")
continue
def dequeue(self):
while True:
with self.lock:
if not self.queue:
print("Queue is empty")
return None
try:
value = self.queue.popleft()
print(f"Dequeued: {value}")
return value
except Exception as e:
print(f"Dequeue failed: {e}")
continue
queue = LockFreeQueue()
threads = []
# 여러 스레드가 큐에 값을 추가 및 제거하는 테스트
for i in range(5):
t1 = threading.Thread(target=queue.enqueue, args=(i,))
t2 = threading.Thread(target=queue.dequeue)
threads.extend([t1, t2])
for t in threads:
t.start()
for t in threads:
t.join()
위 코드는 Python에서 락을 이용한 단순한 큐의 동작을 모사한 것입니다. LockFreeQueue 클래스는 Python의 deque를 사용하여 큐를 관리하며, enqueue와 dequeue 연산에서 락을 사용하여 스레드 안전성을 확보합니다. 이 예제에서는 완전히 락 프리라고 말할 수는 없지만, 락을 최소화하여 락 프리 구조의 특징을 설명하는 데 도움이 됩니다.
핵심 부분: CAS 연산
락 프리 큐의 핵심은 원자적 연산을 사용하여 상태를 업데이트하는 것입니다. Python에서 직접 CAS 연산을 구현하기는 어렵지만, C나 Go 언어에서는 stdatomic.h 헤더를 사용하거나 sync/atomic 패키지를 사용하여 쉽게 CAS 연산을 구현할 수 있습니다.
다음은 Go 언어에서 CAS를 사용한 락 프리 큐의 예입니다:
package main
import (
"fmt"
"sync/atomic"
)
type Node struct {
value int
next *Node
}
type LockFreeQueue struct {
head *Node
tail *Node
}
func (q *LockFreeQueue) Enqueue(value int) {
node := &Node{value: value}
for {
tail := q.tail
next := tail.next
if next == nil {
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&tail.next)), unsafe.Pointer(next), unsafe.Pointer(node)) {
atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail)), unsafe.Pointer(tail), unsafe.Pointer(node))
return
}
} else {
atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail)), unsafe.Pointer(tail), unsafe.Pointer(next))
}
}
}
func main() {
q := &LockFreeQueue{
head: &Node{},
tail: &Node{},
}
q.Enqueue(10)
fmt.Println("Value enqueued successfully.")
}
Go 언어에서의 atomic.CompareAndSwapPointer는 C의 CAS 연산과 유사하게 동작하며, 락 프리 큐 구현에 사용됩니다. 이렇게 함으로써 큐의 상태를 안전하게 업데이트할 수 있습니다.
락 프리 큐의 장단점
장점
- 높은 성능: 락이 없으므로 스레드들이 병렬적으로 작업을 수행할 수 있어 성능이 향상됩니다.
- 데드락 방지: 락을 사용하지 않기 때문에 데드락이 발생하지 않습니다.
단점
- 구현의 복잡성: 락 프리 자료구조는 일반적으로 구현이 매우 복잡합니다. 원자적 연산을 사용해야 하고, 여러 스레드 간의 동기화 문제를 직접 처리해야 하기 때문에 어려울 수 있습니다.
- 아키텍처 의존성: 락 프리 알고리즘은 종종 하드웨어 아키텍처에 의존적입니다. 특히 원자적 연산은 CPU 명령어에 따라 다르게 구현될 수 있습니다.
마무리
락 프리 큐는 고성능이 필요한 멀티스레드 환경에서 중요한 역할을 합니다. 비록 구현이 복잡하고 디버깅이 어려울 수 있지만, 락으로 인한 성능 저하나 데드락의 위험을 줄이는 데 매우 효과적입니다. 이 포스팅을 통해 락 프리 큐의 개념을 이해하고, 간단한 예제를 통해 원자적 연산과 큐의 작동 방식을 배울 수 있기를 바랍니다.
'개발 이야기 > 아키텍처' 카테고리의 다른 글
고성능 LLM 서비스 구현을 위한 GPU 커널 튜닝 전략 (0) | 2024.11.23 |
---|---|
MSA(Microservices Architecture) 이해하기: HTTP와 gRPC를 통한 통신 방식 (1) | 2024.11.18 |
# Kubernetes Cluster Architecture (0) | 2021.05.02 |
# MSA(Micro Service Architecture) 서비스 간 통신 (0) | 2020.05.19 |
#1 데이터베이스의 무결성을 보장해주는 WAL(Write-Ahead Logging) (2) | 2020.04.20 |