7 ms·
I'm not trying to prove anything, I'm asking why you "had concurrency problems from using the STL" when the STL doesn't have anything to do with concurrency.
by CyberDildonics 1mo ago
I'm not trying to prove anything, I'm asking why you "had concurrency problems from using the STL" when the STL doesn't have anything to do with concurrency.
- jstimpfle 1mo agoYou have repeatedly proven, and continue to do so, also by way of your exchanges with other commenters, that you're not asking out of curiosity. After all the previous comments we've exchanged, your line of asking was, "Which part of the STL are you expecting to be thread safe?" i.e. you were continuing to assume that I was somehow naive or uneducated. I did not assume anything to be thread safe in the way you imply (I used a simple mutex based approach to protect accesses). If you'd been asking in good faith, the question would have been, "how did the concurrency problems look like"? To which I'm going to answer, one of the bugs I hit was due to unexpected invalidation of a std::deque iterator. This came from being mislead to use std::deque as a quick & dirty implementation of a producer-consumer queue, and keeping iterators to track the read and write positions. Almost nobody has actually used std::deque (I hadn't either) but there is a common understanding (perhaps misunderstanding) that it is something like a chunk-queue. That vague understanding led me to believe that I can (and should, to avoid O(n) random access ) keep iterators after write operations. And using them that way did work for quite some time, I only hit confusing issues later. (Actually random access is specified to be O(1) but this is even less widely known and makes std::deque a quite arcane data structure). The problem with an abstract iterator interface here is that it doesn't help understanding what std::deque actually is. In case of std::deque, keeping read and write cursors works mostly fine, but it stops working (for example) if the read cursor pointed to the current end (was equal to deque::end) and the deque gets an append, which will invalidate the old end (read) cursor. This is a good example of the complexity we have to deal with if we don't want to write a simple straightforward solution from scratch (chunk list) but instead code against something that we don't understand well. Not trying to use the STL but instead doing straightforward low level code would have made potential pitfalls more clear, and would have made bug search easier. It would have required less work to get the code to a working and maintainable state. Another problem with std::deque is that the sizes of the chunks are not specified. They vary wildly between implementations, such that you can in practice get no performance guarantees from using std::deque, unless committing to a specific STL implementation (which is rarely practical). In fact, it is not even specified that deque uses something like chunks internally. It's too abstract to be useful.
- CyberDildonics 1mo agoTo which I'm going to answer, one of the bugs I hit was due to unexpected invalidation of a std::deque iterator. This isn't a problem with the standard library because a std::dequeue or any other core data structure doesn't make any promises about concurrency. You can find details of iterator invalidation here. https://cppreference.com/cpp/container/deque https://cppreference.com/cpp/container/deque If you have an underlying data structure that is being used from multiple threads, you can't hold on to raw pointers into the data structure. There is no way for other threads to know that it can't be changed, moved, freed or invalidated. You need to copy the data out while a mutex is still locked (if doing simple mutex style concurrency) or you need to hold a reference count in the object that is returned so that the underlying structure knows that it can't touch that data from other threads. I hope it isn't lost on you that the reference counting approach is much easier to do with a destructor, since the reference count can be incremented before it is given to you from the API and decremented automatically when it goes out of scope. If you want some good concurrent queues for C++, look at this person's work: https://github.com/cameron314/concurrentqueue https://github.com/cameron314/concurrentqueue
- jstimpfle 1mo agoSerious question, are you an AI programmed to be annoying? > This isn't a problem with the standard library because a std::dequeue or any other core data structure doesn't make any promises about concurrency. Dude, I KNOW I need to handle concurrency myself. But I'd contend the point that it isn't a problem with the STL: It is a bug (that I introduced myself) that I had to deal with because of complexity, or rather because non-obvious behaviour, because bullshit boilerplate. > If you have an underlying data structure that is being used from multiple threads, you can't hold on to raw pointers into the data structure. There is no way for other threads to know that it can't be changed, moved, freed or invalidated. This is totally irrelevant because if you paid attention, the problem wasn't even threads. It was concurrency, more abstractly. Iterator invalidation based on the "manifested" order of execution. But anyway, you want to jump to reference counting. I'd say you can absolutely hold on to raw pointers from multiple threads, it entirely depends on what you do. If the threads have unpredictable lifetimes, then yes, some form of reference counting is indicated. But when you know that isn't the case, then it isn't the case and you probably don't need reference counting. > I hope it isn't lost on you that the reference counting approach is much easier to do with a destructor, since the reference count can be incremented before it is given to you from the API and decremented automatically when it goes out of scope. Except when you're passing around stuff and have to duplicate or move references, and have to use APIs that receive pre-incremented or un-incremented pointers. In some cases your data structures might even be so messy that you end up with cycles. I have my scars from making my own COM pointer classes with copy and move semantics, and also from using "official" COM pointer classes. After a couple of iterations I've decided to cut all the boilerplate and C++ ceremony that doesn't do anything, and get rid of ugly method wrappers that are a pain to step through in the debugger, and stopped clinging to a cargo cult which simply leaves you with harder to detect bugs. You heard right, I'm back to completely manual reference counting (and only counting where I _have_ to), somehow the code is much shorter and easily understandable, I got back control over what happens. Have been able to keep atomic ops at a minimum, with RAII superfluous ops can happen easily. (Remember Chromium's 25000 copies per keystroke bug?) And there has only been a single instance where I introduced a leak, that was immediately pointed out by the D3D11 debug layer. I'm doing this approach for my second project already and have found it to work great. There is no solution except good understanding of what you do, and good code structure that expresses this understanding. Generic "RAII" type understanding is rarely helpful IMO, you give up control and sometimes end up throwing hands in the AIIR and hope it will not break. > https://github.com/cameron314/concurrentqueue https://github.com/cameron314/concurrentqueue Thanks for the pointers to what is probably 5K lines of C++ boilerplate. But I have written half a dozen concurrent queues myself, locking and lock-free ones. Some in less than a hundred lines. Also one in ~2K lines, that was for a longer-term project where the queue needs to safely persist to disk every couple of milliseconds, while ingesting millions of messages per second and billions of bytes per second (was hitting the ~2GB/s that I could get out of my flash drive). If you want an approachable source that leaves out the fluff, I'd recommend 1024cores by Dmitry Vyukov (only issue is formatting).