Can’t you do this in Rust with std::sync::Mutex and similar?
Can’t you do this in Rust with std::sync::Mutex and similar?
What’s more interesting is figuring out why that is. Also why Arc<T> is Send only if T is Send.
- RwLock hands out multiple references (that's the point), Sync means a type can be used from multiple threads (concurrently), if RwLock<T: !Sync> was Sync it would allow multiple outstanding references for the same !Sync object, which is not legal.
- Mutex, however, only hands out a single reference at a time (which is also why it can always hand out a mutable reference), meaning semantically it acts like it moves the T to the target thread then borrows it = nothing to sync, that's why Mutex<T> is Sync if T is Send.
- For Arc, if it were Send without the wrapped object being Send it would allow Send-ing !Send objects: create an Arc<T>, clone it, move the clone to a second thread, drop the source, now you can try_unwrap() or drop() the clone and it'll work off of the second thread.
This is a problem with threadlocal state, but also with resource affinity (e.g. on windows a lock's owner is recorded during locking, and only the owner can release the lock, there are also lots of APIs which can only work off of the main thread.), or thread-safety (Rc for instance would be completely broken if you could Send it, as the entire point is to use an unsynchronised refcount).
In fact even without actually putting anything of substance in the mutex, you can get value from type system judo using this mechanism, which C++ doesn't appear to do either.
[[no_unique_address]] since C++20. Before that there was the empty base class optimization.
Can you say whether you genuinely thought C++ had ZSTs? And if so, how you came to that conclusion ?
Thanks for pointing me to Boost synchronized_example<T> showing that this does exist, at least as an experimental library feature.
This is the synchronized value pattern [1]. I'm pretty sure that my 3rd edition of "The C++ programming Language" by Bjarne had a description of it and it predates rust by at least a decade.
[1] https://www.boost.org/doc/libs/1_80_0/doc/html/thread/sds.ht...
That Boost link says it is "experimental and subject to change in future versions" but I don't know whether Boost just says that about everything or whether this would particularly mark out this feature.
† In Rust Mutex<T> is 8 bytes bigger than T, typically. In C++ std::mutex is often 40 bytes.
The boost warning doesn't have much to it. Boost libraries don't even guarantee API stability across versions and boost.synchronized has been available for a few years with no changes.
> In Rust Mutex<T> is 8 bytes bigger than T, typically. In C++ std::mutex is often 40 bytes.
That's because on libstdc++ std::mutex embeds a pthread_mutex_t which is 40 bytes for ABI reasons. It is a bad early ABI decision that can't be changed unfortunately. std::mutex on MSVC is worse. std::shared_mutex is much smaller on MSVC, but even worse than std::mutex on libstdc++.
A portable Mutex<T> of minimal size can be built on top of std::atomic::wait though.
We have to be realistic, the standard library will never be complete and you'll always have to get basic components from 3rd party or write them yourself.
That way, such ‘bleeding edge’ features evolve and improve a lot before being set in stone.
I'd guess that while a modern class are probably taught some sort of open addressed hash map, they aren't being taught anything as exotic as Swiss Tables or F14 (Google Abseil and Facebook Folly's maps) but that's OK because standardising all the fine details of those maps would be a bad idea too.
On the other hand, the document does not tell you to use a halfway decent hash function, and many standard implementations don't provide one, so in practice many programs don't use one. The "bad hash map" performs OK with a terrible hash function, whereas the modern ones require decent hashes or their performance is miserable.
for (auto foo : bar.wlock()) {
baz(foo);
}
You need to do bar.withWLock([](auto &lockedBar) {
for (auto foo : lockedBar) {
baz(foo);
}
});
In rust, the lifetime checking prevents that.The other big gotcha is accidentally blocking while holding a lock. E g. Instead of
auto g = bar.wlock();
baz(*g);
co_await quux();
You should do {
auto g = bar.wlock();
baz(*g);
}
co_await quux();
Or use withWLock. If you co_await with the lock held you can deadlock if the executor switches to a different coroutine that tries to acquire the same lock. If you actually need to hold the lock across the blocking call, you need coroutine-aware locks, which turns it into auto g = co_await bar.wlock();
baz(*g);
co_await quux();
No idea if rust prevents this problem - I suspect not, but I haven't used async rust.Tokio provides a Mutex like your final example that is intended for use in such async code that will hold locks while waiting because Tokio will know you are holding the lock. It is accordingly more expensive, and so should only be used if "Don't hold locks while asleep" was not a practical solution to your problem.