Hacker News new | ask | show | jobs
by efokschaner 1647 days ago
This is the first time I've heard of "thread compatible" objects/state. The author states: "Most types in C++ are thread-compatible, as this guarantee comes mostly comes for free", and that "Any concurrent call to a non-const method must be synchronized by the caller."

My understanding is that due to things like memory tearing, or the possibility of invariants between object members being in intermediate states, reads are always unsafe when there is a possibility of writes. Therefore, you can't selectively skip having some kind of synchronisation for the const calls (the reads), as something needs to be synchronizing all the reads against any potential writes. So to me this just reads as, "the caller has to manage all the thread safety for all the accesses", which I'd agree is what typically "comes for free" in c++, but I'd hardly call it a "guarantee".

I'm wondering if I'm not seeing some meaningful level of "thread compatibility" in between "thread-safe" and the "accessor beware" behaviour of all c++ that wasn't designed to be thread-safe. Or is there an example of some c++ that is not even "thread-compatible" once the caller has accepted the burden of synchronization?

3 comments

> reads are always unsafe when there is a possibility of writes.

That is correct for any reads that could race against a write. Writes must be synchronized with all other operations. But reads may proceed in parallel with other reads without synchronization.

For example, if you have an object that is written in the thread where it is constructed, and only released to other threads once the mutation is complete, then you do not need to synchronize readers. Because the nature of the data flow ensures that none of the writes can race with the reads.

> So to me this just reads as, "the caller has to manage all the thread safety for all the accesses"

Not all accesses. Just the ones where a writer may race with another write or a read.

> Or is there an example of some c++ that is not even "thread-compatible" once the caller has accepted the burden of synchronization?

Thread-unsafe types do not even allow two reads to be unsynchronized.

Thanks for the response. I think my disagreement is in describing that as "without synchronization", somewhere the caller is somehow ensuring there are no writes during their safe parallel reads.
I agree that the writes are synchronized with the reads in my example. But the reads are totally unsynchronized with respect to other reads, in every sense. That is what thread-compatibility guarantees you: concurrent reads without synchronization. A thread-unsafe type doesn't allow even that much.
Isn't it always safe if you only read? I can't imagine an example where a race occurs when all threads are just reading.
"Read" here is shorthand for "call a const method". You can write const methods that are not safe to call concurrently: for example, they could use "mutable" or "const cast" to perform mutation, or they could access a non-const pointer to shared state. If a type had a const method like this, the type would no longer be considered thread-compatible.
Thanks, that makes sense.
> My understanding is that due to things like memory tearing, or the possibility of invariants between object members being in intermediate states, reads are always unsafe when there is a possibility of writes.

Correct. Rust calls this "aliasing XOR mutability". You can either have any number of readers reading the data as constant, or you can have one writer mutating the data. You can never have reads and writes at the same time. (unless you use, e.g., a mutex, which allows writes that _look like_ reads from the type system's PoV, which is safe)

I interpreted the author saying "thread-compatible" to mean "accessor beware". Surprisingly in some cases, in C++ it's unsafe for multiple threads to even call const member functions on the same object, for example when the const method modifies a `mutable` field. One way this could happen is when a "const" method lazily computes a value and stores it in a field.