본문으로 건너뛰기
Folly Code Review · 37/89

folly::AtomicHashMap — lock-free read 분석

· Hawk · 4분 읽기

#한 줄 요약

folly::AtomicHashMapappend-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); // capacity
ahm.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-free
auto [it, ok] = m.insert(42, "answer");
// 3. find — wait-free
auto it = m.find(42);
if (it != m.end()) {
std::cout << it->second;
}
// 4. operator[] — insert default if absent
m[7]; // ""가 들어감
// 5. erase 없음
// m.erase(42); // 컴파일 에러
// 6. 통계
m.size();
m.maxSize(); // 처음 지정한 capacity

AtomicHashMap은 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.h
template <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 level
std::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
EraseXO
Resizeappend-onlyfull rehash
Value 수정 (in-place)atomic type만자유
Iterator 안정영구 (cell 그대로)rehash 시 invalid
Reader contention0mutex 필요

이런 제약을 받아들일 수 있는 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 내부에는 있으나 미공개
// folly
folly::AtomicHashMap<K, V> ahm(N); // append-only, lock-free read
folly::ConcurrentHashMap<K, V> chm; // 다음 글 — 전체 thread-safe

abseil은 concurrent hash map을 공개하지 않는다. fbcode는 두 가지(AtomicHashMap / ConcurrentHashMap)로 다른 trade-off를 제공.

#코드 리뷰 포인트

// Bad — erase 필요한데 AtomicHashMap
folly::AtomicHashMap<int, Conn> conns(N);
// disconnect 시 erase 안 됨 — 메모리 누수
// Good — erase 필요하면 ConcurrentHashMap
folly::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 / atomic
folly::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를 제공한다.

#관련 항목

Folly Code Review · 38 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전