Skip to content

Atomicity: lock-free guarantees #300

Closed
@jfbastien

Description

@jfbastien

TL;DR: make all accesses of 1, 2 and 4 bytes "always" lock-free, wider access "maybe".

C++11 has a concept of "atomicity" where an operations is either entirely observable, or not at all. This guarantee is on a per object basis, during the object's lifetime—for our purpose this is equivalent to per memory location. This is irrespective of the object's size, but does require proper natural alignment (UB if not naturally aligned).

Not all hardware have load/store of wide sizes (indeed, atomics can have any size!), the language therefore offers an is_lock_free method on the std::atomic class. This is a per-type property, but it's effectively based on size. This method is determined at runtime (i.e. it is not constexpr), but it's effectively known at load time because it can't change during program execution.

Using an atomic of non-atomic size relies on the runtime using a global lock (or a shard of global locks) to seamlessly prevent partial reads or writes. Normal loads/stores are unaffected, only atomics of unusual sizes go through this lock.

C has ATOMIC_*_LOCK_FREE macros for BOOL, CHAR, CHAR16_T, CHAR32_T, WCHAR_T, SHORT, INT, LONG, LLONG, POINTER, which can be used at compile time. These macros are a tribool: never lock-free, sometimes lock free, always lock free.

There is a proposal to add constexpr atomic::is_always_lock_free to the languages.

Note: things are a bit more complicated because C11 decided to standardize is_lock_free as taking a pointer to the object as well. Implementations ignore that pointer, I suggest we do the same.

In all 3 cases we (WebAssembly implementors) need to decide what we want to guarantee. The runtime checks are easy (look at current machine, return that), the shard of global locks is easy (it's an implementation detail), but the compile-time guarantee of "yes this will always be lock free on all platforms WebAssembly will ever support" isn't that easy to make, and it's binding forever.

Having such a guarantee is pretty important: it's silly to write a lock-free algorithm if the datastructure is atomic through a lock (i.e. isn't actually lock free!). A good example for 64-bit lock-free being useful is a doubly-linked list where 2 pointers use up 64 bits.

@sunfishcode points out:

  • ppc32, ppc64, x86-32, x86-64, sparcv9, systemz, bpf, mips32, mips64, arm32, and arm64 can all do int32 lock-free.
  • ppc64, x86-64, arm64, sparcv9, systemz, bpf, and mips64 can all do int64 lock-free.

I think we can safely say 1, 2 and 4 byte accesses are always lock-free (even if you can't do a byte load you can always cmpxchg a wider size).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions