Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> For this to happen, the store and load must be endowed with release and acquire semantics respectively. To that end, we have the "store-release" and "load-acquire" operations.

I suppose release and acquire semantics are baked into Rust as the language's "ownership" concept. A variable is moved (release) out of the calling scope and the function takes ownership (acquire) at the function call, which synchronizes the symmetric operations. In the pthread_create/pthread_join example, thread 2 is borrowing the variable "s".



Sort of. Rust actually has special marker traits called Send and Sync which tell the compiler if a value can be passed between threads, and if so; if it can be accessed concurrently. It is the job of a type which implements Send/Sync to actually guarantee that any access to that type satisfies what the trait implies, as well as the rest of Rust's ownership and borrow semantics.

For example, the difference between Rc and Arc is precisely this marker trait. Rc just holds a box with a reference count and some other object in it, while Arc actually enforces atomic access to the reference count. You can't hold an Rc in multiple threads because it doesn't Sync accesses, and you can't Send it across threads because someone else might hold the same box. Meanwhile, both types do not provide mutable access to their contents at all: you need another locking primitive to ensure the contents of the box follows ownership and borrow semantics as the compiler intended, if only by punting the check to runtime.

(And yes, that means Rust technically has single-threaded locks. They're still useful, albeit extremely painful to work with.)


acquire/release semantics restricts reordering of other loads and stores. That is, `a=1; b.store(2, release);` here the release operation on b affects the previous store to a. Rust's ownership model cannot express this, as it treats separate variables as independent.

Your analogy to borrowing is correct in the pthread case, because it has no race. But typically lockless programming has races, and I don't think it's useful to think of it in terms of ownership.


Acquire/release semantics are an invention. They did not exist in ARMv7. I don't know the exact history of when or where that model became popular.

When I was in college in the mid 00s, reorderings were largely discussed in terms of "load/load", "load/store", "store/load" and "store/store" reorderings.

Turns out that thinking of things in terms of "load/load", "load/store", "store/load" and "store/store" is really hard and kind of unhelpful.

----------

I know that by C++11, acquire/release was finally formalized as a concept (maybe the C++11 committee borrowed the idea from somewhere else). I know the C++11 committee was heavily influenced by Java's memory model.

From my perspective, "acquire/release" semantics became popular BECAUSE RAII is popular in C++. RAII in C++ became ownership in Rust.

So in a sense, acquire/release is a cousin of ownership and RAII. But acquire/release was invented to solve a problem: a way to discuss reorderings without having to think about the awful load/load or store/store model that was popular in the 00s and 90s.

--------

Look at the memory fence instructions that were written long before C++11.

https://www.felixcloutier.com/x86/lfence

> Performs a serializing operation on all load-from-memory instructions that were issued prior the LFENCE instruction. Specifically, LFENCE does not execute until all prior instructions have completed locally, and no later instruction begins execution until LFENCE completes.

------

Now compare it to memory fence instructions written after C++11, like LDAR (ARM64)

> An LDAR instruction guarantees that any memory access instructions after the LDAR, are only visible after the load-acquire. A store-release guarantees that all earlier memory accesses are visible before the store-release becomes visible and that the store is visible to all parts of the system capable of storing cached data at the same time.

Formalizing the concept of "acquire" vs "release" really makes these ordering ideas more concrete to work with. The "acquire" barriers are the part needed when entering a critical section, and "release" barriers are the bit needed when leaving a critical section.

--------

No, its not identical to Rust Ownership or RAII from C++, but its kinda-sorta similar. More similar, and therefore more comfortable, to work with. More comfortable than "loads are ordered" (lfence) and "stores are ordered" (sfence) instructions at least.


I just can't see any connection between RAII and acquire/release. For example both C and the Linux kernel have acquire/release, neither have RAII.

I think the terms "acquire" and "release" came from their connection to locking. Linux even used to call acquire and release fences "lock" and "unlock"; here's where it got changed: https://lore.kernel.org/linux-arch/20131217092435.GC21999@tw...


I thought "acquire" just meant "a load cannot be reordered before this barrier" and "release" just meant "a store cannot be reordered past this barrier"?


> "acquire" just meant "a load cannot be reordered before this barrier"

I'm pretty sure its "loads / stores cannot be reordered before this barrier". And technically, its "load-acquire", because the load-acquire is what the ordering is relative against. The C++ model does not match assembly-language of various architectures (though ARM64 was designed with C++11's memory model in mind)

Ditto with store-release. "loads/stores cannot be reordered past the store-release".

---------

    // Writing to protectedValue1 must occur after the lock.
    lock(); // load-acquire is part of all locks
    protectedValue1 = foobar();
    localVariable = protectedValue2;
    unlock(); // store-release is part of all unlocks
    // Reading protectedValue2 must occur before the store-release. If the protectedValue2 read occurs here, then some other thread may modify it.
------------

Citation: LDAR and STLR from ARM64, which applies to "any memory access".


That, plus the synchronization effects of the two operations on the same address. It’s important to note that another thread which doesn’t access the same atomic with the appropriate ordering can observe an inconsistent state, even if it puts a full fence between each non-atomic operation (on architectures with weak memory models).


I don’t think the intuition you get from Rust’s ownership or general RAII semantics will serve you well for understanding acquire and release semantics. Fundamentally they are about memory accesses other than the atomic operation they are applied to. And the scope of this relationship is not directly specified: it’s “all stores before this point” or “all loads after this point”. I don’t see what the analogue in RAII would be; RAII refers to a single object.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: