Skip to content

Folly Synchronized and Utility Library

Source paths: references/impl/folly/folly/Synchronized.h, folly/Function.h

Synchronized<T>: Type-Safe Lock Guard

cpp
template <typename T, typename Mutex = SharedMutex>
class Synchronized {
  mutable Mutex mutex_;
  T datum_;

public:
  // 写锁
  LockedPtr wlock() {
    return LockedPtr(this, std::unique_lock(mutex_));
  }
  // 读锁
  LockedPtr rlock() const {
    return LockedPtr(this, std::shared_lock(mutex_));
  }
  // 带超时的写锁
  std::optional<LockedPtr> wlock(Duration timeout) {
    std::unique_lock lk(mutex_, timeout);
    if (!lk.owns_lock()) return std::nullopt;
    return LockedPtr(this, std::move(lk));
  }
};

LockedPtr holds the lock and provides operator-> and operator* to access the data. It is impossible to bypass the lock and access the data at compile timedatum_ is a private member.

  wlock() 写锁获取流程:
    调用者── wlock() ──→ Synchronized
                          ├──→ std::unique_lock(mutex_)
                          │       │
                          │    mutex_ locked (独占)
                          │       │
                          │    构造 LockedPtr(this, lock)
                          │       │
    调用者←── LockedPtr ──┘
    lp->push_back(4)   ← 通过 operator-> 直接访问 datum_
    ~LockedPtr()       ← 作用域结束,自动解锁

  rlock() 读锁(可并发读):
    线程 A── rlock() ──→ shared_lock(mutex_)
    线程 B── rlock() ──→ shared_lock(mutex_)
    两个线程同时读 datum_ ...

Function: Move-Only SBO Callable

cpp
template <typename Signature>
class Function;  // 类似 std::function,但 move-only

// 关键差异:
// - std::function 要求 callable 可拷贝
// - folly::Function 支持 move-only callable(如捕获 unique_ptr 的 lambda)
// - 两者都有 SBO(Small Buffer Optimization),24 字节

Key differences from std::function:

Dimensionfolly::Functionstd::function
CopyNot allowed (move-only)Requires copyable
SBO buffer24 bytes24 bytes
Move semanticsTrue moveObjects within SBO still require copying
nullptr checkoperator bool()operator bool()
Use caseAsync callbacks (inherently move-only)General-purpose callbacks

The move-only design of folly::Function better matches the actual patterns of asynchronous programming — most callbacks capture resources like unique_ptr<Connection> and should not be copied.

User API

This article covers the user-facing entry points of folly::Synchronized<T> and folly::Function: the former exposes lock-protected access, while the latter exposes a move-only callable wrapper.

Standard Semantics

Synchronized<T> and Its Correspondence with Standard Lock Guards

Synchronized<T, Mutex> bundles data and a mutex into a single object, which has no direct equivalent in the standard library. The standard approach uses separate declarations:

cpp
// 标准模式:锁与数据分离,程序员自行保证配对
std::mutex mtx;
std::vector<int> data;
{
  std::lock_guard lk(mtx);
  data.push_back(1);
}

Semantic extensions of Synchronized<T>:

  • Compile-time access enforcement: datum_ is a private member, accessible only through the LockedPtr returned by wlock()/rlock(). The standard library's lock_guard does not constrain which data it protects.
  • First-class read-write lock support: rlock() returns a shared_lock-based LockedPtr, allowing concurrent readers. The standard library requires manual composition of shared_mutex + shared_lock.
  • Automatic Mutex concept detection: Via SFINAE (kSynchronizedMutexIsShared, etc.), the Mutex is identified at compile time as exclusive, shared, or upgradeable, and the corresponding SynchronizedBase specialization is automatically selected, exposing wlock/rlock or only lock.
  • Timeout locks return optional: wlock/rlock overloads with a Duration parameter return a LockedPtr holding a null value (checked via isNull() / operator bool()), rather than returning a bool like std::unique_lock::try_lock.

Semantic Narrowing and Extension of folly::Function

Relative to std::function<R(Args...)>:

Semantic Dimensionstd::functionfolly::Function
CopyabilityRequires copy-constructible callableCopy prohibited (move-only)
const correctnessCan invoke non-const operator() on a const referenceconst signature (R(Args...) const) requires callable to have const operator() const; no bypass allowed
const → non-constImplicit conversion safe (gives up const call capability)
non-const → constRequires explicit constCastFunction(), preventing mutable lambdas from escaping into const contexts
noexcept signaturesFrom C++17 onward, noexcept does not participate in the type systemR(Args...) noexcept is a distinct type, distinguishable via template specialization
Empty stateoperator bool() checkSame, and the source object is empty after a move

Core semantic narrowing: The move-only constraint of folly::Function means it cannot be used in containers requiring copies (e.g., std::vector copy assignment scenarios), but it better fits the lifecycle model of asynchronous callbacks — callbacks are inherently about transferring resource ownership.

Object Layout

The mutex_ + datum_ structure of Synchronized<T> and the SBO configuration of folly::Function have been shown above; lock guard object and function wrapper internal state diagrams will be added later.

Core Source Paths

Synchronized.h and Function.h were given at the beginning of this article; lock guard factories, storage policies, and call dispatch entry points will be added later.

Core Classes / Functions

Synchronized<T, Mutex> Class

cpp
template <typename T, typename Mutex = SharedMutex>
class Synchronized : SynchronizedBase<Synchronized<T,Mutex>, kSynchronizedMutexLevel<Mutex>> {
  mutable Mutex mutex_;
  T datum_;
};
  • CRTP inheritance from SynchronizedBase, automatically selecting the base class specialization based on the Mutex capability level (Unique/Shared/Upgrade).
  • Default Mutex = folly::SharedMutex, supporting wlock() + rlock(); if replaced with std::mutex, only lock() is available.

LockedPtr<LockedType, LockPolicy>

cpp
template <class LockedType, class LockPolicy>
class LockedPtr {
  LockedType* parent_;              // 指向 Synchronized 实例
  SynchronizedLockType<...> lock_;  // unique_lock / shared_lock / upgrade_lock
};
  • RAII guard: acquires the lock on construction, releases on destruction. Not copyable, not movable (determined by lock_ semantics).
  • operator->() / operator*() return a reference to datum_; const correctness is jointly determined by LockPolicy and whether LockedType is const.
  • isNull() / operator bool(): returns a null pointer (parent_ == nullptr) on timeout or failed tryLock.

wlock() / rlock() Factory Paths

SynchronizedBase::wlock() → constructs LockedPtr<Subclass, LockPolicyExclusive> → internally calls std::unique_lock(mutex_) to acquire an exclusive lock.

SynchronizedBase::rlock() → constructs LockedPtr<Subclass, LockPolicyShared> → internally calls std::shared_lock(mutex_) to acquire a shared lock.

SynchronizedBase::tryWLock() / tryRLock() → uses SynchronizedLockPolicyTry* + std::try_to_lock, returning a nullable LockedPtr.

Timed overloads → the LockedPtr constructor accepts a duration parameter and calls mutex_.try_lock_for().

withWLock() / withRLock()

cpp
auto result = syncObj.withWLock([](auto& datum) {
  datum.push_back(42);
  return datum.size();
});

Accepts a folly::Function and invokes it while the lock is held. Safer than manually managing a LockedPtr — the lambda scope is the critical section, and the return value is copied out before the lock is released.

folly::Function<R(Args...)>

cpp
template <typename R, typename... Args>
class Function<R(Args...)> {
  detail::function::Data data_;       // union: tiny (SBO) / big (heap)
  R (*invoker_)(Data&, Args...);      // 调用分发函数指针
  void (*manager_)(Data&, Data&, Op); // 生命周期管理(move/nuke/heap)
};
  • invoker_: points to a compile-time-generated type-erased call entry that extracts the callable from Data and forwards arguments.
  • manager_: handles three operations — Op::MOVE (move to new Data), Op::NUKE (destruct + deallocate), Op::HEAP (return heap pointer, for std::move_only_function interoperability).

constCastFunction()

cpp
Function<R(Args...) const> constCastFunction(Function<R(Args...)>&&) noexcept;

Explicitly converts a non-const Function to a const signature. Move semantics ensure the source object is cleared. This is the only permitted path from non-const to const.

Key Algorithms

Lock Acquisition/Release RAII Path

wlock() 构造路径:
  SynchronizedBase::wlock()
    → LockedPtr(this)                    // 构造函数
      → SynchronizedLockType<Exclusive>(mutex_)  // 即 unique_lock(mutex_)
        → mutex_.lock()                  // 阻塞等待独占
      → parent_ = this
  作用域结束
    → ~LockedPtr()
      → ~unique_lock()
        → mutex_.unlock()               // 释放独占

The rlock() path is identical, except it uses shared_lock(mutex_)mutex_.lock_shared(), allowing multiple readers to hold the lock simultaneously.

tryLock / Timed Lock Branch

tryWLock():
  → LockedPtr(this, std::try_to_lock)
    → unique_lock(mutex_, std::try_to_lock)  // 非阻塞
    → 若 !owns_lock() → parent_ = nullptr   // 空 LockedPtr

wlock(timeout):
  → LockedPtr(this, timeout)
    → unique_lock(mutex_, timeout)            // 阻塞但有截止
    → 若 !owns_lock() → parent_ = nullptr

Callers must check if (auto lp = obj.tryWLock()) { ... }; calling operator->() on an empty LockedPtr will dereference a nullptr.

folly::Function Construction / Move / Call Dispatch

Construction (small object SBO path):

Function f = [small_lambda] { ... };
  → sizeof(lambda) <= 6 * sizeof(void*) ?
    YES → placement new 到 data_.tiny 内联缓冲区
          manager_ = &Traits::manage<small_lambda, Op::MOVE/NUKE>
    NO  → operator new 分配堆内存
          data_.big = heap_ptr
          manager_ = &Traits::manage<small_lambda, Op::MOVE/NUKE>
  → invoker_ = &Traits::invoke<small_lambda>

Move:

Function g = std::move(f);
  → manager_(g.data_, f.data_, Op::MOVE)
    → 若在 SBO 内:placement-move-construct 到新 tiny
    → 若在堆上:g.data_.big = f.data_.big; f.data_.big = nullptr
  → manager_(f.data_, f.data_, Op::NUKE)
    → 对源执行析构(SBO 内)或置空(堆上已转移)
  → g.invoker_ = f.invoker_; g.manager_ = f.manager_;
  → f.invoker_ = nullptr; f.manager_ = nullptr;  // 源置空

Call:

f(args...);
  → invoker_(data_, std::forward<Args>(args)...)
    → 从 data_.tiny 或 data_.big 取出 callable
    → 转发调用

Op::HEAP is used for interoperability between folly::Function and std::move_only_function — querying the heap pointer to enable ownership transfer.

ABI Constraints

Both components are pure header-file templates and do not provide a stable binary ABI contract. ABI constraints arise from the following aspects:

Object Layout Changes

  • The layout of Synchronized<T, Mutex> is mutex_ + datum_ (ordering and alignment are determined by the compiler). Changing the Mutex type or the layout of T alters the size and offset of all translation units using that instance.
  • The layout of folly::Function<R(Args...)> is a Data union (6 × void* = 48 bytes on 64-bit) plus two function pointers (invoker_ + manager_). The SBO buffer size 6 * sizeof(void*) is a hardcoded constant and may change across different folly versions.
  • Data::BigTrivialLayout contains {void*, size_t, size_t} (24 bytes); Data::tiny is 48 bytes of aligned storage. Any adjustment to these sizes is an ABI break.

Header Inline Dependencies

  • wlock(), rlock(), LockedPtr::operator->(), etc. are all inline functions. Their implementations are directly included in the caller's .o files. Upgrading folly headers without recompiling all dependents leads to ODR violations.
  • folly::Function's invoker_ and manager_ function pointers are bound to concrete callable types at compile time. Different translation units instantiating the same folly::Function must use exactly the same version of the headers.

Coupling with Standard Library Lock Types

  • LockedPtr embeds std::unique_lock / std::shared_lock. The layouts of these standard lock guards are inconsistent across standard library implementations (libstdc++, libc++, MSVC STL).
  • Therefore, the binary compatibility of Synchronized<T> across different standard libraries depends on the layout of the underlying lock guards — it is inherently non-portable.

Practical Recommendations

In scenarios requiring a stable ABI (dynamic library boundaries), Synchronized<T> and folly::Function should be wrapped behind a pimpl or used only internally. Interfaces exposed to the outside should use C ABI or standard library types.

Exception Safety

Synchronized<T>

  • Exception during lock acquisition: When the constructor of std::unique_lock / std::shared_lock calls mutex_.lock(), if the mutex operation itself throws std::system_error (e.g., EDEADLK), the lock guard does not release the lock if it was never held — exception safety is guaranteed by the standard lock guard (basic guarantee). LockedPtr has not been fully constructed at this point, and datum_ will not be accessed.
  • Exception in withWLock callback: LockedPtr is destructed on the withWLock stack frame, releasing the lock. If copying the callback's return value throws, the lock is still safely released — the basic guarantee holds. If the return value type has a noexcept move constructor, there is no exception risk.
  • datum_ construction failure: If the constructor of Synchronized<T> throws because T's construction failed, mutex_ as an already-constructed member is automatically destructed by the compiler, with no resource leaks.

folly::Function

  • Target callable construction failure: When assigning or constructing a folly::Function, if the callable's move constructor throws, the Function remains in an empty state (operator bool() == false). SBO uses placement new, which does not leak on failure; heap allocation uses operator new, and std::bad_alloc propagates upward on failure.
  • Move construction does not throw: The move constructor and move assignment of folly::Function are marked noexcept — SBO uses std::move (memcpy for trivially movable types), and heap transfers pointers. Therefore, folly::Function itself satisfies nothrow move.
  • Exception during call: invoker_ forwards directly to the user's callable, and exceptions propagate as-is. folly::Function neither catches nor modifies exceptions thrown by the callable.
  • Calling an empty Function: Calling operator() on an empty (moved-from or default-constructed) folly::Function triggers folly::throwBadFunctionCall(), equivalent to std::bad_function_call.

Summary: Synchronized<T> provides the basic exception guarantee (locks are always safely released); folly::Function's move operations are noexcept, and assignment/construction provide the basic guarantee (if the target fails to construct, the empty state is preserved).

Iterator / Reference Invalidation

LockedPtr Lifetime and Reference Invalidation

LockedPtr::operator->() / operator*() return a reference to datum_. The valid lifetime of this reference is strictly bound to the LockedPtr's scope:

cpp
auto& ref = *syncObj.wlock();   // 危险:临时 LockedPtr 立即析构
ref.push_back(1);               // UB:锁已释放,ref 悬挂

auto lp = syncObj.wlock();      // 正确:lp 作用域内锁持有
lp->push_back(1);               // OK
  • LockedPtr is neither movable nor copyable, so its lifetime cannot be extended beyond the current scope.
  • References inside withWLock's lambda are safe — the lambda ends when the lock is released, and references do not escape.
  • If data needs to be exported from the critical section, the copy/move should be completed inside the lambda rather than returning a reference:
cpp
auto copy = syncObj.withRLock([](const auto& d) { return d; }); // 拷贝出来

folly::Function Target Replacement Invalidation

  • folly::Function& operator=(F&&) and folly::Function& operator=(Function&&) first destruct the old target (manager_(data_, data_, Op::NUKE)), then construct the new target. After assignment completes, any state of the old callable is destroyed.
  • If the callable captures pointers/references to its own internal state, those pointers become dangling after assigning a new target — this is the user's responsibility, similar to iterator invalidation rules for containers.
  • After move assignment, the source folly::Function is empty (operator bool() == false), and calling operator() on it throws bad_function_call.

Container analogy: LockedPtr reference ≈ std::vector::iterator, lock release ≈ vector reallocation — the trigger for reference invalidation is the end of resource lifetime, not a data structure change.

Performance Model

Shared Lock vs Exclusive Lock Contention

  • Read-heavy, write-light scenarios: rlock() uses shared_lock, allowing multiple readers to execute concurrently. folly::SharedMutex's default implementation is reader-prioritized, providing throughput far superior to std::mutex under read-intensive loads.
  • Write contention scenarios: wlock() is an exclusive lock, serializing all write operations. If the critical section is too long (e.g., performing I/O while holding the lock), writers will block all readers.
  • Fairness: folly::SharedMutex supports a Priority template parameter (reader-prioritized / writer-prioritized / fair alternating). The default reader-prioritized policy may cause write starvation; high write loads should consider writer-prioritized.
  • Value of tryLock: tryWLock() / tryRLock() are non-blocking, avoiding thread suspension under heavy contention — suitable for optimistic update patterns (fallback/retry on failure).

folly::Function SBO Hit Rate

  • SBO threshold: Data::tiny is 6 * sizeof(void*) = 48 bytes (64-bit system). Typical objects that fit:

    • Non-capturing lambda / function pointer: 8 bytes ✅
    • Lambda capturing 1-2 ints/pointers: 16-24 bytes ✅
    • Lambda capturing std::string (SSO ≤ 15 characters): ~40 bytes ✅
    • Lambda capturing std::unique_ptr + small data: ~24 bytes ✅
    • Lambda capturing std::vector or large objects: > 48 bytes ❌ → heap allocation
  • SBO performance advantage: When SBO hits, construction/move/destruction all happen on the stack with no malloc/free calls. Move operations degrade to memcpy (for trivially movable types), taking about 1-2 ns.

  • Heap allocation cost: When SBO misses, each construction triggers a malloc (about 50-100 ns), moves only transfer pointers (~1 ns), but destruction still requires free.

Benefits and Costs of Move-Only Avoiding Copies

  • Benefits: When folly::Function captures resources like unique_ptr, no deep copy is needed. Compared to std::function requiring a copyable callable, move construction avoids unnecessary heap allocation and data duplication.
  • Costs: Move-only semantics prohibit copy operations in containers like std::vector. emplace_back must be used instead of push_back, and when vector reallocates, each element is moved rather than copied — this is usually an advantage, but if the callable's move constructor has side effects (e.g., updating external state), care is needed.
  • Benchmark difference from std::function: When the callable is lightweight (SBO hit), both perform similarly; when the callable is heavyweight (heap allocation), folly::Function's moves are faster (pointer transfer vs deep copy).

libstdc++ vs libc++ vs MSVC

Synchronized<T> vs Raw Mutex Pattern

DimensionRaw mutex + separate dataSynchronized<T>
Compile-time enforcementNone (relies on programmer discipline)datum_ is private, accessible only through lock guards
Read-write lock supportRequires manual shared_mutex + shared_lock compositionrlock() / wlock() as first-class APIs
Lock granularityFree to choose the protection scopeEntire datum_ is lock-protected (finer granularity requires splitting instances)
Performance overheadZero extra overheadOne extra indirection (LockedPtrdatum_), but compilers typically inline it away

folly::Function vs Three std::function Implementations

Dimensionlibstdc++ (GCC)libc++ (Clang)MSVC STLfolly::Function
SBO buffer16 bytes (_M_invoker + _M_manager)24 bytes (__buf_)16 bytes (_Storage)48 bytes (6 × void*)
SBO max object size~16 bytes (function pointer only)~24 bytes~16 bytes~48 bytes (fits medium lambdas)
Move semanticsObjects within SBO require copying (memcpy when is_trivially_copyable)Objects within SBO require copyingObjects within SBO require copyingTrue move within SBO (move-construct)
const correctnessoperator() always constoperator() always constoperator() always constconst/non-const separation (R(Args...) const as distinct type)
noexcept signaturesNot distinguishedNot distinguishedNot distinguishedDistinguished (R(Args...) noexcept as distinct template instantiation)
move-only callableNot supportedNot supportedNot supportedSupported
Call on emptyUndefined / std::__throw_bad_function_callabort() or UBstd::_Xbad_function_callfolly::throwBadFunctionCall()

Key difference: folly::Function's SBO buffer is that of libstdc++/MSVC and that of libc++, significantly increasing the hit rate for medium-sized callables. The const correctness separation is a design unique to folly::Function — the standard library's std::function allows invoking a non-const callable through a const reference, which is semantically questionable.

C++23 std::move_only_function: Fixes the move-only deficiency of std::function, but the SBO buffer is still smaller, and it does not support const/non-const signature separation. folly::Function remains stricter in terms of const correctness.

Minimal Reproduction Code

cpp
#include <folly/Function.h>
#include <folly/Synchronized.h>

int main() {
  folly::Synchronized<int> value(1);
  auto locked = value.wlock();
  *locked += 1;

  folly::Function<int(int)> fn = [](int x) { return x + 1; };
  return fn(*locked);
}

Compile / Disassembly / Benchmark Evidence

Lock Guard Inline Path

wlock() / rlock()LockedPtr construction → unique_lock / shared_lock construction → mutex_.lock() / mutex_.lock_shared(). The entire chain is typically inlined by the compiler under -O2 into a single atomic operation (for the fast path of folly::SharedMutex):

; wlock() 快速路径(x86-64, -O2)
lock cmpxchg [rdi], 0x1    ; 原子 CAS 获取独占锁
jne   .slow_path            ; 争用时走慢路径

After inlining, operator->() / operator*() degenerate to a direct pointer offset (parent_ + offsetof(datum_)), with zero extra overhead.

folly::Function SBO / Heap Allocation Switch Point

cpp
// SBO 命中(≤ 48 字节)
folly::Function<void()> f = []{};   // sizeof(lambda) = 1 → SBO
// 反汇编:placement new 到栈上,无 malloc 调用

// 堆分配(> 48 字节)
char big[100];
folly::Function<void()> f = [big]{};  // sizeof(lambda) = 100 → malloc
// 反汇编:call operator new → mov [rbx+48], rax  // data_.big = ptr

Benchmark Comparison: folly::Function vs std::function

Typical benchmark scenario (single-threaded, loop calling 10M times):

Operationstd::function (GCC)folly::Function
Construction (SBO hit)~3 ns~3 ns
Construction (heap alloc)~80 ns~80 ns
Move~3 ns (SBO copy)~1 ns (SBO memcpy or pointer transfer)
Call~3 ns (indirect call)~3 ns (indirect call)
Destruction (SBO)~1 ns~1 ns
Destruction (heap)~50 ns (free)~50 ns (free)

The core difference is in moves: std::function's in-SBO moves require copy-constructing the callable (memcpy when is_trivially_copyable), while folly::Function always uses move-construct. For non-trivially-movable callables, folly::Function's move performance is significantly better.

SBO hit rate difference: folly::Function's 48-byte SBO achieves nearly 100% hit rate for typical callback lambdas (capturing 1-3 pointers), while libstdc++'s 16-byte SBO falls back to heap allocation for lambdas capturing medium-sized objects like std::string.

cpplings Exercise Entry Points

Released under the MIT License