Skip to content

스트라이프 락

스트라이프 락(lock striping)은 하나의 자료구조 전체를 보호하는 단일 락 대신, 자료구조를 여러 구역으로 나누고 구역마다 독립적인 락을 두는 기법이다. 락을 여러 줄(stripe)로 쪼갠다는 의미에서 이런 이름이 붙었다.

공유 자료구조를 락 하나로 보호하면(coarse-grained locking) 구현은 단순하지만, 스레드가 많아질수록 모든 스레드가 같은 락을 두고 경합(contention)한다. 서로 전혀 다른 데이터를 건드리는 작업조차 직렬화되므로, 코어를 늘려도 처리량이 늘지 않는 병목이 된다.

반대로 원소마다 락을 두면(fine-grained locking) 경합은 사라지지만 락 자체의 메모리 오버헤드가 커지고 관리가 복잡해진다.

스트라이프 락은 그 중간 지점이다. 락을 N개만 만들어두고, 접근하려는 데이터의 해시값으로 어떤 락을 잡을지 결정한다.

lockIndex = hash(key) % N

서로 다른 stripe에 속한 데이터에 대한 작업은 병렬로 진행되고, 같은 stripe에 해시된 데이터끼리만 경합한다. 락 개수 N을 조절해서 메모리 오버헤드와 동시성 수준을 트레이드오프할 수 있다.

해시맵을 16개의 stripe로 나눈 경우를 생각해 보자.

buckets: [0] [1] [2] [3] ... [63]
locks: [0] [1] [2] ... [15]
put("apple") → hash % 16 = 3 → locks[3] 획득
put("banana") → hash % 16 = 7 → locks[7] 획득 // apple과 병렬 실행 가능
put("cherry") → hash % 16 = 3 → locks[3] 대기 // apple과 경합

이론적으로 최대 16개의 쓰기 작업이 동시에 진행될 수 있다.

간단한 구현은 다음과 같다.

template <typename K, typename V>
class StripedMap {
static constexpr size_t N_LOCKS = 16;
std::vector<Node<K, V>*> buckets;
std::array<std::mutex, N_LOCKS> locks;
std::mutex& lockFor(const K& key) {
return locks[std::hash<K>{}(key) % N_LOCKS];
}
public:
V get(const K& key) {
std::lock_guard<std::mutex> guard(lockFor(key));
// 해당 버킷 탐색
}
void put(const K& key, const V& value) {
std::lock_guard<std::mutex> guard(lockFor(key));
// 해당 버킷에 삽입
}
};
  • Java ConcurrentHashMap: Java 7까지는 맵을 16개의 Segment로 나누고 각 Segment가 자체 락을 갖는 전형적인 스트라이프 락 구조였다. Java 8부터는 Segment를 없애고 버킷(첫 노드) 단위의 synchronized + CAS로 바뀌어, 사실상 버킷 수만큼의 fine-grained locking에 가까워졌다.
  • Linux 커널, 데이터베이스: 해시 테이블이나 버퍼 풀을 여러 파티션으로 나누고 파티션별 락을 두는 패턴이 널리 쓰인다. PostgreSQL의 buffer partition lock, InnoDB의 buffer pool instance 등이 같은 아이디어다.
  • 전역 연산이 어렵다. size(), clear(), 리사이징처럼 자료구조 전체를 봐야 하는 연산은 모든 stripe의 락을 잡아야 한다. 비용이 크고, 그래서 ConcurrentHashMap.size()는 정확한 스냅샷 대신 근사치를 반환하는 식으로 설계됐다.
  • 여러 락을 잡을 때는 순서를 고정해야 한다. 두 키에 대한 락을 동시에 잡아야 하는 작업(예: 계좌 이체)에서 스레드마다 획득 순서가 다르면 데드락이 생긴다. 락 인덱스 오름차순으로 잡는 식으로 순서를 강제해야 한다. Guava의 Striped.bulkGet()이 이런 정렬을 대신 해준다.
  • 해시 분포가 나쁘면 효과가 없다. 특정 stripe에 키가 몰리면(hot key) 그 stripe가 단일 락과 다를 바 없는 병목이 된다.
  • 락 개수는 보통 2의 거듭제곱으로 잡아서 모듈러 연산을 비트 마스크로 대체한다. 동시 접근 스레드 수보다 충분히 크게 잡되, 무작정 늘리면 메모리와 캐시 지역성에서 손해를 본다.

참고