folly::AtomicHashMap — lock-free read 분석
#한 줄 요약
folly::AtomicHashMap은 append-only + lock-free read의 hash map이다. erase 불가, value 수정도 제한적. 대신 read는 mutex 없이 atomic load만으로 끝나 contention이 0.
#동기
설정 cache, dictionary lookup, monitoring counter 같이 기록은 가끔, 읽기는 매우 자주인 데이터는 일반 mutex 기반 map이 과한 비용. 16 reader 가 동시에 read 한다면 mutex가 줄을 세우게 된다.
typical use case - feature flags : startup 시 fill, 그 후 read-only - id → name table : 새 id 가끔 추가 - per-thread counter : append, increment, 읽기 위주AtomicHashMap의 제약:
- erase 불가.
- key의 in-place 갱신은 atomic value type만.
- 최대 capacity를 처음에 지정.
대신 얻는 것:
- read는 wait-free.
- writer 한 명이 들어와도 reader 영향 0.
folly::AtomicHashMap<int64_t, std::string> ahm(100'000); // capacityahm.insert(1, "alice");// reader thread들auto it = ahm.find(1);if (it != ahm.end()) Use(it->second);#API & 사용법
#include <folly/AtomicHashMap.h>
// 1. 생성 — 최대 capacity 미리 지정folly::AtomicHashMap<int64_t, std::string> m(1'000'000);
// 2. insert — concurrent, lock-freeauto [it, ok] = m.insert(42, "answer");
// 3. find — wait-freeauto it = m.find(42);if (it != m.end()) { std::cout << it->second;}
// 4. operator[] — insert default if absentm[7]; // ""가 들어감
// 5. erase 없음// m.erase(42); // 컴파일 에러
// 6. 통계m.size();m.maxSize(); // 처음 지정한 capacityAtomicHashMap은 sub-map들의 chain 구조. capacity를 초과하면 새 sub-map을 추가. 단, 더 이상 ScalableAlloc 없이 fixed.
#내부 구현
#AtomicHashArray가 기본 단위
AtomicHashMap └ AtomicHashArray (sub-map 1) └ AtomicHashArray (sub-map 2, 첫 게 가득 차면 추가) └ ...각 AtomicHashArray는 고정 크기 array. open addressing linear probing.
// 약식 — folly/AtomicHashArray.htemplate <class Key, class Value>class AtomicHashArray { struct Cell { std::atomic<Key> key; Value value; // value는 atomic 아닐 수도 }; std::vector<Cell> cells_; // fixed capacity std::atomic<size_t> numEntries_;};key는 std::atomic. Empty sentinel(보통 0 또는 ~0)을 사용. CAS로 key를 lock 후 value 채움.
#Insert — CAS based
// 약식std::pair<Iterator, bool> insert(Key k, Value v) { size_t h = hash(k); size_t start = h % capacity_; for (size_t i = 0; i < capacity_; ++i) { size_t idx = (start + i) % capacity_; Cell& c = cells_[idx];
Key expected = kEmpty; if (c.key.compare_exchange_strong(expected, k, std::memory_order_release)) { // empty 였음 → claim 성공 c.value = std::move(v); numEntries_.fetch_add(1); return {Iterator{this, idx}, true}; } if (expected == k) { // 이미 있음 return {Iterator{this, idx}, false}; } // 다른 key → 다음 probe } // 가득 — sub-map 추가 로직 (AtomicHashMap level)}CAS 한 번으로 claim. 성공하면 value를 채운다. value 쓰기는 atomic 아님 — reader가 partial value를 볼 가능성. 그래서 value는 POD 또는 immutable이 권장.
#Find — wait-free
// 약식Iterator find(const Key& k) const { size_t h = hash(k); size_t start = h % capacity_; for (size_t i = 0; i < capacity_; ++i) { size_t idx = (start + i) % capacity_; Key found = cells_[idx].key.load(std::memory_order_acquire); if (found == k) return Iterator{this, idx}; if (found == kEmpty) return end(); } return end();}원자 load 한 번. lock 없음. acquire/release pairing으로 insert side가 write한 value가 reader에 보임.
#Multi sub-map chain
// AtomicHashMap levelstd::pair<Iterator, bool> insert(Key k, Value v) { // 1. 가장 최근 sub-map에 시도 for (auto& sub : subMaps_) { auto r = sub.insert(k, v); if (r.second || r.first != sub.end()) return r; } // 2. 가득 → 새 sub-map (mutex로 한 번에 한 명만) std::lock_guard g(growLock_); subMaps_.emplace_back(make_unique<AtomicHashArray>(growSize)); return subMaps_.back()->insert(k, v);}성장은 mutex 보호 하지만 read/insert는 기존 sub-map에서 lock-free.
#제약
| 항목 | AtomicHashMap | 일반 hash map |
|---|---|---|
| Capacity 미리 지정 | 필수 | reserve로 hint |
| Erase | X | O |
| Resize | append-only | full rehash |
| Value 수정 (in-place) | atomic type만 | 자유 |
| Iterator 안정 | 영구 (cell 그대로) | rehash 시 invalid |
| Reader contention | 0 | mutex 필요 |
이런 제약을 받아들일 수 있는 use case에서만.
#std/abseil 비교
// std// 직접 대응 없음. std::unordered_map + std::shared_mutex로 구현 필요
// abseil// flat_hash_map + absl::Mutex// 또는 absl::node_hash_map + node 자체에 lock// concurrent variant은 없음 — Google 내부에는 있으나 미공개
// follyfolly::AtomicHashMap<K, V> ahm(N); // append-only, lock-free readfolly::ConcurrentHashMap<K, V> chm; // 다음 글 — 전체 thread-safeabseil은 concurrent hash map을 공개하지 않는다. fbcode는 두 가지(AtomicHashMap / ConcurrentHashMap)로 다른 trade-off를 제공.
#코드 리뷰 포인트
// Bad — erase 필요한데 AtomicHashMapfolly::AtomicHashMap<int, Conn> conns(N);// disconnect 시 erase 안 됨 — 메모리 누수
// Good — erase 필요하면 ConcurrentHashMapfolly::ConcurrentHashMap<int, Conn> conns;erase가 의미 있으면 AtomicHashMap을 쓰면 안 된다. 또는 tombstone value 같은 logical delete 패턴.
// Bad — 큰 value, partial read 위험folly::AtomicHashMap<int, std::vector<int>> m(N);m.insert(1, {1, 2, 3, 4, 5});// reader가 인덱스만 본 상태에서 size를 다르게 볼 수 있음
// Good — POD / atomicfolly::AtomicHashMap<int, uint64_t> counters(N);value는 atomic 이거나 immutable after insert. 그렇지 않으면 reader가 깨진 값을 본다.
// 잘못된 capacity 추정folly::AtomicHashMap<int, int> m(1000);for (int i = 0; i < 1'000'000; ++i) m.insert(i, i);// sub-map이 계속 추가 — find가 모든 sub-map을 순회, 느려짐capacity는 최대값에 맞춰 한 번에. sub-map chain이 길어지면 find가 모든 chain을 본다.
#안티패턴
ConcurrentHashMap대신 AtomicHashMap: erase가 필요하면 ConcurrentHashMap. AtomicHashMap은 단방향 transactional 환경만.- value mutation: in-place update는 value가
std::atomic이어야 안전. 일반 type은 reader가 torn value를 본다. - capacity 부족: chain이 길어지면 read complexity 증가. 처음에 max에 맞춰 reserve.
#정리
AtomicHashMap은 lock-free read, append-only insert.- erase 불가, value mutation 제약.
- read-heavy + 거의 안 지우는 데이터에 ideal.
- capacity를 처음에 지정 (초과 시 sub-map chain, find 비용 증가).
- value는 atomic 또는 immutable이어야 reader 안전.
#다음 편
ConcurrentHashMap은 sharded mutex로 erase 포함 full thread-safe를 제공한다.
#관련 항목
- Part 8-04: ConcurrentHashMap — 전체 thread-safe variant
- Part 9-02: SharedMutex — multi-reader 패턴 일반
- 원문 — folly/AtomicHashMap.h
Folly Code Review · 38 of 89
- 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
- 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
- 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
- 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
- 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
- 6 Folly production validation 문화 — peta-scale에서 단련된 코드
- 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
- 8 folly::Promise·makeFuture — Future를 만드는 두 길
- 9 folly::SemiFuture vs Future — executor binding의 명시화
- 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
- 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
- 12 folly::Future retry·window·via — 제어 흐름 조합자
- 13 folly::fibers 분석 — M:N stackful coroutine
- 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
- 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
- 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
- 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
- 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
- 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
- 20 folly::IOBufQueue — chain의 push/pull 추상화
- 21 folly::io::Cursor·RWCursor — chain 위의 stream
- 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
- 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
- 24 folly::FBString 분석 — SSO + COW 구현
- 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
- 26 folly::StringPiece — string_view 호환 분석
- 27 folly Join·Split utilities — 문자열 분해와 결합
- 28 folly::to·tryTo — text↔num 변환 분석
- 29 folly Conv Customization — 사용자 타입 지원
- 30 folly Conv 성능 비교 — sprintf·stringstream 대비
- 31 folly::F14ValueMap vs std::unordered_map
- 32 folly::F14NodeMap — stable pointer가 필요할 때
- 33 folly::F14VectorMap — cache-friendly iteration
- 34 folly::F14FastMap — auto-select 동작
- 35 folly F14 internals — SIMD probing 메커니즘
- 36 folly::small_vector — inline storage 분석
- 37 folly::FixedString — compile-time string
- 38 folly::AtomicHashMap — lock-free read 분석
- 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
- 40 folly::EvictingCacheMap — LRU 구현 분석
- 41 folly::Synchronized — lock wrapper 패턴
- 42 folly::SharedMutex 분석
- 43 folly::Baton — one-shot wait 동기화
- 44 folly::RWSpinLock 분석
- 45 folly::PicoSpinLock — 1-byte spinlock
- 46 folly::ProducerConsumerQueue — SPSC 큐 분석
- 47 folly::MPMCQueue — multi-producer multi-consumer
- 48 folly::UnboundedQueue — 동적 크기 lock-free
- 49 folly::fibers::Channel — Go-like channel
- 50 folly::dynamic — JSON-like dynamic type 분석
- 51 folly JSON conversion — toJson·parseJson
- 52 folly dynamic ↔ struct — manual marshaling
- 53 folly dynamic Visitor pattern — type별 분기
- 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
- 55 folly::SingletonVault 분석 — 등록·소멸·의존성
- 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
- 57 folly::ExceptionWrapper — type-erased exception holder
- 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
- 59 folly::Optional vs std::optional
- 60 folly::Function vs std::function
- 61 folly::Lazy — 지연 초기화 wrapper
- 62 folly Meta 스타일 code review 패턴
- 63 folly anti-patterns — 잘못 쓰면 std보다 느림
- 64 folly vs std 선택 기준 분석
- 65 folly::coro 개요 — production C++20 코루틴 어댑터
- 66 folly::coro::Task — lazy single-shot 코루틴
- 67 folly::coro::AsyncGenerator — 비동기 스트림
- 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
- 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
- 70 folly::Expected — 결과 또는 오류
- 71 folly::Try — Future 결과 wrapper
- 72 folly::Try vs Expected 선택 기준
- 73 folly::Range — 일반 iterator pair
- 74 folly::Uri — URL 파서
- 75 folly Fingerprint64·128 — 분산 hash
- 76 folly SpookyHashV2 — fast non-crypto hash
- 77 folly::Init — main() 부트스트랩
- 78 folly::Indestructible — global lifetime 패턴
- 79 folly::MicroLock — 1-byte 락
- 80 folly::MicroSpinLock — 가장 좁은 spin lock
- 81 folly::format — legacy formatter 분석
- 82 folly::demangle — typeid 디망글링
- 83 folly::DynamicConverter — dynamic ↔ struct
- 84 folly::RecordIO — append-only 로그 파일 포맷
- 85 folly::io::Compression — zstd·lz4·snappy wrapper
- 86 folly::AsyncIO — io_uring·Linux AIO
- 87 folly::CancellationToken — 코루틴·Future 취소 전파
- 88 folly::observer — hot config의 atomic refresh
- 89 fbcode 패턴 모음 — folly 사용의 실전
관련 글
folly::UnboundedQueue — 동적 크기 lock-free
Part 10-03: UnboundedQueue — linked segment 기반 동적 크기 lock-free 큐. SPSC 모드에선 거의 무비용으로 성장한다.
같은 시리즈에서 이어 읽기
folly::MPMCQueue — multi-producer multi-consumer
Part 10-02: MPMCQueue — ticket 기반 lock-free 큐. CAS 없이 여러 producer/consumer를 안전하게 처리한다.
같은 시리즈에서 이어 읽기
folly::ProducerConsumerQueue — SPSC 큐 분석
Part 10-01: ProducerConsumerQueue — SPSC lock-free ring buffer. cache line padding, acquire/release만으로 RTT을 줄이는 패턴.
같은 시리즈에서 이어 읽기