동시성 컬렉션 개요

동시성 컬렉션 개요

동시성 컬렉션의 필요성

멀티스레드 프로그래밍에서는 여러 스레드가 동일한 데이터에 접근하고 수정하는 상황이 빈번하게 발생합니다. 이때 데이터의 무결성integrity 과 일관성consistency 을 유지하기 위해서는 스레드 간의 동기화가 필요합니다. 그러나 전통적인 동기화 방식인 락lock은 성능 저하와 교착 상태deadlock 등의 문제를 야기할 수 있습니다. 이를 해결하기 위해 .NET은 동시성 컬렉션을 제공하여, 여러 스레드가 안전하게 데이터에 접근하면서도 높은 성능을 유지할 수 있도록 지원합니다.

동시성 컬렉션의 발전 배경

기존의 제네릭 컬렉션(List, Dictionary 등)은 멀티스레드 환경에서 안전하지 않기 때문에, 개발자가 명시적으로 락을 사용하여 동기화 해야 했습니다. 그러나 락은 다음과 같은 문제를 가지고 있습니다.

  • 성능 저하: 락은 스레드 간의 경합을 발생시켜 성능을 저하시킵니다.
  • 교착 상태: 잘못된 락 사용은 교착 상태를 초래할 수 있습니다.
  • 복잡성 증가: 락을 적절히 관리하는 것은 코드의 복잡성을 높입니다. 이러한 문제를 해결하기 위해 .NET Framework 4.0에서는 락 없이도 스레드 안전성을 보장하는 동시성 컬렉션Concurrent Collections 이 도입되었습니다. 이 컬렉션들은 내부적으로 락-프리lock-free 또는 미세한 락fine-grained lock을 사용하여 성능과 안전성을 모두 확보합니다.

동시성 컬렉션의 종류와 특징

ConcurrentDictionary<TKey, TValue>

  • 특징: 키-값 쌍을 안전하게 저장하고 업데이트할 수 있는 사전 구조입니다.
  • 용도: 스레드 간에 공유되는 데이터의 추가, 업데이트, 삭제가 빈번한 경우에 적합합니다.

ConcurrentQueue<T>

  • 특징: FIFOFirst-In-First-Out 방식으로 데이터를 안전하게 관리합니다.
  • 용도: 작업 큐, 메시지 큐 등 순차적인 처리 순서가 중요한 경우에 적합합니다.

ConcurrentStack<T>

  • 특징: LIFOLast-In-First-Out 방식으로 데이터를 안전하게 관리합니다.
  • 용도: 역순으로 데이터를 처리해야 하는 경우에 적합합니다.

ConcurrentBag<T>

  • 특징: 순서를 보장하지 않는 컬렉션으로, 각 스레드가 독립적으로 데이터를 추가하거나 제거할 수 있습니다.
  • 용도: 순서가 중요하지 않고, 빠른 추가와 제거가 필요한 경우에 적합합니다.

동시성 컬렉션의 동작 원리

다중 스레드 환경에서 Concurrent 컬렉션은 데이터 일관성과 최신 상태의 가시성을 유지하면서 높은 성능을 달성하도록 설계되었습니다. 이를 위해 Concurrent 컬렉션은 메모리 모델과 락-프리 알고리즘을 활용하여 동기화lock 없이도 데이터를 안전하게 처리할 수 있습니다.

락-프리 알고리즘

동시성 컬렉션은 락-프리lock-free 또는 부분적으로 락-프리를 구현하여, 경합이 발생하지 않는 경우 락 없이 작업을 처리할 수 있도록 설계되었습니다. 이는 공유 자원에 락을 걸지 않고도 동시 접근을 허용하여 성능을 최적화하는 기법입니다.

CAS 연산

  • CASCompare-And-Swap는 락-프리 알고리즘의 핵심 구성 요소로, 자원의 상태를 락 없이 안전하게 업데이트하기 위해 사용됩니다
  • 예상 값과 현재 값을 비교하여 일치하면 새로운 값으로 교체하는 원자적 연산입니다.
  • 여러 스레드가 동시에 변수의 값을 변경하려고 할 때, CAS를 사용하면 하나의 스레드만 성공적으로 값을 변경할 수 있습니다.

락과 CAS의 차이점

  • 락은 공유 자원을 보호하기 위해 스레드 간 경합을 차단하는 방식으로, 락이 걸리면 다른 스레드는 자원에 접근할 수 없습니다.
  • 반면 CAS 연산은 락을 사용하지 않으면서도 자원을 안전하게 업데이트할 수 있도록 하는 비차단non-blocking 연산으로, 여러 스레드가 경합 상태에서도 각자의 작업을 시도할 수 있습니다.

락-프리 알고리즘의 장점

  • 락을 사용하지 않으므로, 락으로 인한 병목 현상이 줄어들어 성능이 개선됩니다.
  • 대량의 스레드가 동시에 데이터에 접근할 때 경합이 줄어듭니다.

락-프리 알고리즘의 단점

  • 락-프리 구조는 구현이 복잡합니다.
  • 일부 경우에는 경합을 완전히 없애지 못해 충돌이 발생할 수 있습니다.
  • CAS 연산이 반복적으로 실패하면 스핀락Spinlock처럼 CPU 리소스가 낭비될 수 있습니다.

Atomicity와 원자적 업데이트

락-프리 알고리즘은 원자적 업데이트가 보장됩니다. 따라서 동시성 컬렉션은 안전한 동시성을 보장하며, 원자적으로 데이터가 추가되고 제거됩니다.

원자성 보장

  • 원자적 연산은 중단되지 않고 한 번에 수행되는 연산입니다.
  • CAS 연산은 중간 상태 없이 완료되거나 실패하므로, 여러 스레드가 동시에 접근하더라도 일관성 있는 결과를 유지할 수 있습니다.

원자적 업데이트

  • CAS를 사용하여 값을 업데이트할 때, 기존 값이 기대한 값과 일치할 때만 값을 변경합니다.
  • 이로 인해 업데이트 과정에서 중간 상태가 노출되스레드풀과의 통합 및 고성능 데이터 수집 Concurrent 컬렉션들은 쓰레드풀과 통합하여 데이터를 수집하고 분산 작업을 처리하는 데 적합합니다. 여러 작업이 동시에 처리되는 환경에서도 스레드 안전성을 유지할 수 있어, 쓰레드풀이 작업을 효율적으로 분산하고 수집할 수 있습니다.

Lazy Initialization과 Double-Checked Locking

지연 초기화Lazy Initialization는 필요할 때까지 객체를 생성하지 않고, 필요한 시점에 생성하는 방식이며, Double-Checked Locking은 객체가 이미 생성되었는지 두 번 확인하여 불필요한 락 획득을 방지합니다. 두 방식은 동시성 컬렉션과 함께 사용하면 메모리 사용을 최적화할 수 있습니다.

데이터 파이프라인 구현

데이터 파이프라인 패턴은 여러 단계로 이루어진 데이터 처리를 순차적으로 진행하는 방식입니다. 각 단계에서 데이터를 처리하고, 다음 단계로 전달하는 방식으로 동작합니다. Concurrent Collection은 각 단계에서 데이터를 저장하고, 여러 스레드가 동시에 읽고 쓸 수 있어 파이프라인 방식에 적합합니다.