락 프리 큐(Lock-Free Queue)에 대한 이해와 예제 코드

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를 사용하여 큐를 관리하며, enqueuedequeue 연산에서 락을 사용하여 스레드 안전성을 확보합니다. 이 예제에서는 완전히 락 프리라고 말할 수는 없지만, 락을 최소화하여 락 프리 구조의 특징을 설명하는 데 도움이 됩니다.

핵심 부분: 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 명령어에 따라 다르게 구현될 수 있습니다.

마무리

락 프리 큐는 고성능이 필요한 멀티스레드 환경에서 중요한 역할을 합니다. 비록 구현이 복잡하고 디버깅이 어려울 수 있지만, 락으로 인한 성능 저하나 데드락의 위험을 줄이는 데 매우 효과적입니다. 이 포스팅을 통해 락 프리 큐의 개념을 이해하고, 간단한 예제를 통해 원자적 연산과 큐의 작동 방식을 배울 수 있기를 바랍니다.