14 ms·
Too dangerous for C++
- bsdpufferfish 3y agoShared objects interacting across threads is a bad idea. Java did it “safely” forever ago, it just throws a lock on everything
- cyber_kinetist 3y agoData sharing between threads is inherently too much of a complex model for programmers to manage (when systems get complicated enough), that in many cases it is better to think of a solution that avoids it altogether. This is why some concurrency-centric languages (Erlang, Go) choosed to use message passing as the main paradigm, instead of going for locks everywhere at runtime (Java) or an incredibly complex type system that tries to prevents data races at compile time (Rust)...
- pkolaczk 3y agoMessage passing is not any easier or safer though. For every problem with shared memory concurrency you can draw a dual problem in message passing. See: https://songlh.github.io/paper/go-study.pdf https://songlh.github.io/paper/go-study.pdf For me, single-threaded intra-task concurrency using async/await turned out to be safer and also easier to work with than either of the above mentioned concurrency models. Just a single loop with a top level select - everything is sequential and easy to reason about, also no need for any synchronization like locks or shared atomic pointers.
- pornel 3y agoRust's type system for thread safety is actually remarkably simple. Types declare whether they add or remove thread safety (e.g. Mutex adds safety, non-atomic Rc removes). Structs automatically become non-thread-safe if they have non-thread-safe fields. Then all the functions that spawn threads or send data over channels require thread-safe types. The fearless concurrency is real. It reliably prevents data races, use-after-free, and moving of thread-specific data to another thread. It works across arbitrarily large and complex call graphs, including 3rd party dependencies and dynamic callbacks. Plus immutability is strongly enforced, and global mutable state without synchronization is not allowed. It doesn't prevent deadlocks, but compared to data corruption heisenbugs, these are pretty easy — attach a debugger and you can see exactly what deadlocked where.
- bsdpufferfish 3y ago> global mutable state without synchronization is not allowed. That's the java model again. I don't want fearless concurrency, I want intentionally designed threads.
- estebank 3y agoIt's not the Java model because Java just makes every operation atomic (from the point of view of the model, I'm sure the JVM and javac must do optimizations to avoid some of them), while Rust enforces that multi threaded access must be through atomic operations, but if there is no multi threaded access or the type is not meant to be used in a multi threaded context, that information is encoded in the type system. This might sound like an academic distinction, but it is different: the developer is in control. You could even go as far as lie to the type system and claim a racy type is actually thread safe. I wouldn't advice doing so, but I can't stop you.
- bsdpufferfish 3y agoI’m sure the rust version is more ergonomic. It’s great you can do it more safetly. But it’s a bad application design from the start.
- pornel 3y agoThat's an incredibly broad criticism aimed at some hypothetical solutions you imagine, not grounded in what Rust does. No language can stop an imaginary infinitely determined fool. Rust's restrictions, such as strict scopes of references and strongly enforced shared XOR mutable access, prevent many sloppy and careless designs that are possible in Java or C++. Rust also takes advantage of its type system, generics, and ecosystem to offer solid constructs for multi-threading. There are safe data parallelism libraries, task queues, thread pools, scoped threads, channels, etc. Users are well equipped to implement multi-threading properly, and as much as possible Rust steers users towards locally-scoped, immutable or share-nothing solutions.
- kazinator 3y ago> the Rc type does not support being sent between threads So why even have such a thing in a language designed for concurrent programming from the ground up? Arc should be called Rc, and that's it.
- LegionMammal978 3y agoWhy not have such a thing, when it is strictly more performant for any program that has data which isn't accessed concurrently?
- kazinator 3y agoBecause then you need to complicate the compiler with a diagnostic against misuse, which has to work 100% right in all situations and be maintained forever.
- tialaramex 3y agoBecause Rust has a safety culture, and provides threading, it is crucial that that the compiler will reject types you cannot safely send to another thread. So it does. So the "diagnostic against misuse" you're concerned about is a necessary part of the compiler anyway. Indeed, although Rc has this line: impl<T: ?Sized, A: Allocator> !Send for Rc<T, A> {} (which means roughly "You can't send this type to another thread") It also has these lines: // Note that this negative impl isn't strictly necessary for correctness, // as `Rc` transitively contains a `Cell`, which is itself `!Sync`.
- LegionMammal978 3y agoIt's not just Arc<T> vs. Rc<T> that's relevant for thread safety, though. Pretty much any kind of shared mutability requires extra protection (locks or atomicity) to work safely across threads, so there has to be some way to indicate whether or not that extra protection is present. Not to mention objects that interact with FFI, such as mutex locks, which must be unlocked from the same thread. It would be a huge performance drain to demand that "every value everywhere must be usable from every thread".
- notso411 3y ago[dead]
- kevr2d2 3y agoPretty clickbaitey title. It's possible to implement in C++... so it's not "too dangerous" for C++. It's dangerous for people who don't have knowledge of what they're doing in C++; same as in any programming language.
- eslaught 3y agoOn the contrary, I thought it was quite apt. If you follow the article's link to this Stack Overflow answer: https://stackoverflow.com/a/15140227/1614219 https://stackoverflow.com/a/15140227/1614219 Which summarizes a discussion by the C++ standards committee to reject the C++ version of Rc, and one of the main arguments is the risk of Rc code being accidentally included in threaded code. I would point out that this code can include: code you wrote years ago that you forgot includes Rc, code in libraries that was modified internally to use Rc and the authors forgot to mention it, code written by colleagues who aren't familiar with the pitfalls, etc. That's why this isn't a trivial problem to solve.
- tialaramex 3y ago"Too dangerous" doesn't imply impossible. It's too dangerous to parachute off the Eiffel tower. That doesn't mean it's impossible, periodically somebody does it.
- xmcqdpt2 3y agoI'm not a C++ expert but I believe it is not possible in current C++ to implement a pointer type that will cause a compiler error when it is sent to another thread.
- PaulDavisThe1st 3y agoThat's almost certainly true, but first you have to define "send to another thread"
- 3836293648 3y agostd::jthread?
- ok123456 3y ago> Apparently, this is enough of an issue that C++20 added a partial template specialization to std::atomic<std::shared_ptr>. My advice, though, would be "don't do that!". Instead, keep your shared pointer in a single thread, and send copies to other threads as needed. This is to support an atomic lock-free shared_ptr. You can then use this as a building block for building lock-free data structures.
- dureuill 3y agoInteresting, I was lacking this context. Could you provide me with more information about this? I only saw atomic_shared_ptr come up in discussions about bugs up to now.
- ok123456 3y agohttps://www.youtube.com/watch?v=gTpubZ8N0no https://www.youtube.com/watch?v=gTpubZ8N0no The target of this optimization is low-latency code. Rendezvous will not work for that.
- cjensen 3y agoThe two criticism at the end are... odd. First, there is criticism that assigning to a shared_ptr is not synchronized so it would be bad to share a single shared_ptr object between threads. True, but that is no different than literally every other non-atomic object in C++. It's not surprising in any way. Second, there is criticism that assigning to the object pointed at by the shared_ptr is not synchronized between threads. This is odd because that's not actually different than a single thread where there are two shared_ptrs pointing to the same object. That is, even with single threading you have a problem you must be careful about.
- rst 3y agoBut if the Rust versions are as safe as claimed, then you're making the critique of C++ stronger, by pointing out that the pitfalls are easier to fall into than the blog post presents -- for the second, you don't even need threads! (And aliasing is one of the things that Rust's borrow machinery at least tries to address, even in a single-threaded context.) So, is he wrong about the Rust part?
- dureuill 3y agoIn this context, "unsynchronized access" refers to read/write operations happening concurrently on multiple threads, *not* to the shared pointers pointing to different objects as a result of the assignment. Unsynchronized access to the pointed to object will typically cause a specific kind of race condition called a data race, which is undefined behaviour. As it requires threads, it cannot happen in a single-threaded context.
- vasilipupkin 3y agoI am not going to be surprised to be downvoted, but you don't need shared_ptr in C++, that is itself overkill The point of C++ is performance. If you don't need performance, why not just use Java or Python, why use Rust?
- saghm 3y ago> The point of C++ is performance. If you don't need performance, why not just use Java or Python, why use Rust? As a counterpoint, I also don't need to use `Rc` or `Arc` in Rust, and I can get by without reference counting. Why use C++?
- shikon7 3y agoYou use Rust if you want performance (especially because there is no garbage collection), and strong safety guarantees. If you don’t care about safety guarantees and abstractions like shared_ptr, you might just as well use C instead of C++.
- nickysielicki 3y agoThe point of a program is first and foremost to be correct, performance is never more important than that. /dev/null is not in fact webscale.
- brigadier132 3y agoYou don't need performance until you do. Writing slow rust in my experience is just as easy as writing python. If not easier because the libraries are better designed.
- bluGill 3y agoThe point of rust is you get the performance of C++ with additional safety.
- Const-me 3y agoFor example, safe rust forbids code which writes to different elements of the same vector from different CPU cores. C++ compiler has no objections, and doing that is often the best way to parallelize computations.
- PaulDavisThe1st 3y agoFrom the stackoverflow link within TFA: > With GCC when your program doesn't use multiple threads shared_ptr doesn't use atomic ops for the refcount. This is done by updating the reference counts via wrapper functions that detect whether the program is multithreaded (on GNU/Linux this is done by checking a special variable in Glibc that says if the program is single-threaded[1]) and dispatch to atomic or non-atomic operations accordingly. > I realised many years ago that because GCC's shared_ptr<T> is implemented in terms of a __shared_ptr<T, _LockPolicy> base class, it's possible to use the base class with the single-threaded locking policy even in multithreaded code, by explicitly using __shared_ptr<T, __gnu_cxx::_S_single>. You can use an alias template like this to define a shared pointer type that is not thread-safe, but is slightly faster[2]:
- dureuill 3y agoI would rather use the non-atomic shared pointer from Boost[1] linked upthread than a non-standard non-portable implementation detail from GCC, but yes, it exists. You can definitely implement a non-atomic non-threadsafe shared pointer in C++, my point in the article is that actually using it is very error prone. This is supported by the type being excluded from the standard library with one of the reasons being the risk of bugs. [1]: https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/html/smart_ptr.html#local_shared_ptr https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/htm...
- PaulDavisThe1st 3y agoThe extent of the error prone-ness depends entirely on what "send to another thread" means (i.e. precisely how this is done).
- PaulDavisThe1st 3y agoSee also: https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/html/smart_ptr.html#local_shared_ptr https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/htm... i.e. a single-threaded non-atomic shared_ptr Rust fans can dislike on the "C++ has no central library system like crates" all they want, but there's not many things you actually need when programming that don't exist for C++, even if you don't like them not coming in a little box that looks like other little boxes.
- FpUser 3y agoThis. As soon as I need something it is quick online search away. Amount of stuff available for C++ is staggering
- nevi-me 3y agoThe criticism is less about what's available, for there is more available in C++ than Rust. The criticism is about ease of packaging in a cross-platform magnet that is easy enough.
- frozenport 3y agoBoost is extremely popular. I've used it in almost all my projects.
- FpUser 3y agoI have exactly zero problems using the same C++ code on Windows (debug and development) and then building and running it on Linux in production
- 0xfaded 3y agoWhile we're at it, std::atomic<std::shared_ptr<T>> https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2 https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2
- dureuill 3y ago
- stathibus 3y agoThere are valid selling points to rust's safety features, but this just feels like "I use rust because I need my compiler to be my training wheels". More of a self-own than anything.
- bestouff 3y agoSeeing the astounding number of CVEs in C++ code everywhere, everybody needs training wheels.
- aprogr 3y agoMy explanation ? There are way more program written in C/C++ than Rust out there, so statistically more bug are found. About the wheels ? Maybe that's true also for Rust developers: https://www.cvedetails.com/vulnerability-list/vendor_id-19029/product_id-48677/Rust-lang-Rust.html https://www.cvedetails.com/vulnerability-list/vendor_id-1902...
- tialaramex 3y agoThe question is whether proportionally more bugs are found, and the indications are yeah, a lot more. There was an academic study of bugs in the Firefox codebase and they found that first time contributors were far more likely to introduce bugs in C++ than Rust proportionally, with the ratio getting tighter as people have more experience with the codebase. If you've got a team of people who've lived with your C++ codebase for a decade, they're perhaps not introducing more bugs than they would in Rust. You're looking at a list of less than two dozen CVEs over several years across the Rust standard library and tooling. There are no CVEs raised for the analogous C++ behaviour, it's just accepted as normal.
- TheRoque 3y agoI don't see what's wrong with having training wheels. In fact, it's not really training wheels, it's just safeguards, and we all need them as much as possible (with a good balance between this and usability of course)
- 3y ago
- reflexe 3y agoFrom my experience, the biggest footgun with shared_ptr and multi threading is actually destruction. It is very hard to understand which thread will call the destructor (which is by definition a non-thread-safe operation), and whether a lambda is currently holding a reference to the object, or its members. Different runs result different threads calling the destructor, which is very painful to predict and debug. I think that rust suffers from the same issue, but maybe it is less relevant as it is a lot harder to cause thread safety issues there.
- dureuill 3y ago> which is by definition a non-thread-safe operation yes, but at this point, since the reference count is reaching 0, there is supposed to be only that one thread accessing the object being destroyed, so the destruction not being thread-safe should not be a problem. If otherwise, it means there was a prior memory error where a reference to the pointed-to object escaped the shared_ptr. From there the code is busted anyway. By the way it cannot happen in Rust. > Different runs result different threads calling the destructor What adverse effects can happen there? I can think of performance impact, if a busy thread terminates the object, or if there is a pattern of always offloading termination to the same thread (or both of these situations happening at once). I can think of potential deadlocks, if a thread holding a lock must take the same lock to destroy the object (unlikely to happen in Rust where the Arc object would typically contain the object wrapped in its mutex and the mutex wouldn't be reused for locking other parts of the code). There isn't much else I can think of, what do you have in mind? > whether a lambda is currently holding a reference to the object, or its members This cannot happen in Rust. If a lambda is holding a reference to the object, then it either has (a clone of) the Arc, or is a scoped lambda to a borrow of an Arc.
- flohofwoe 3y agoRefcounted memory management on a large scale is slow anyway, with or without atomic refcounting. The bigger problem is that Rc, Arc or shared_ptr often only manage one small object, and that object lives in a separate tiny heap allocation. So you end up with many tiny heap allocations spread more or less randomly around in memory and the likelyhood of getting cache misses on access is much highter than tightly packing the underlying data into arrays and walking over the array items in order. And if you only have a small number of refcounted references in your program, the small performance difference between atomic and non-atomic refcounting doesn't matter either. Same problem with Box and unique_ptr btw, a handful is ok, but once that number grows into the thousands all over the codebase it's hard to do any meaningful optimization (or even figure out how much performance you're actually losing to cache misses because it's a death-by-a-thousand-cuts scenario).