스트라이프 락(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)); // 해당 버킷에 삽입 }};실제 사용 예
Section titled “실제 사용 예”- 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의 거듭제곱으로 잡아서 모듈러 연산을 비트 마스크로 대체한다. 동시 접근 스레드 수보다 충분히 크게 잡되, 무작정 늘리면 메모리와 캐시 지역성에서 손해를 본다.
참고