So, there is Intel's code for implementing a thread-safe queue "one writes one reads."

There, as you noticed, there are constructions of approximately the following type:

const_cast <const volatile node*> (param) //это в load_consume const_cast <volatile node*> (param) // это в store_release. 

The question is, why are these keywords in const_cast needed and how do they differ (and apparently they differ) from the standard construction of the type const_cast <node*> (param) ?


And this does not apply specifically to the question, here are my attempts to explain the code. Perhaps this will help someone understand (but not yet me), why is there a const_cast at const_cast ?

If you are interested in what is there specifically in these two methods, then, as far as I understand, all nodes (namely), namely head , tail , next , tail_copy and first designated as pointers to certain objects constructed in accordance with the structure node (it contains a link to the next node and value). That is, this is all declared as node* and looks like &node elsewhere.

load_consume to its logic, load_consume takes a pointer and returns a value according to it, but in fact this value is also a pointer because of the above things. We also pass a pointer to this pointer as a parameter to this method. It also makes a const_cast , about which I asked, and causes a memory barrier that tells the processors to complete all of its incomplete operations.

Once load_consume used to check if the queue is not empty: it tries to take this pointer to tail->next , if it “could not” (apparently, at the very end is zero), then it is considered that there is nothing more in the queue.

Elsewhere ( alloc_node ) it simply passes another pointer to one pointer.

store_release takes a pointer and a value, then dereferences the first and gives the value of the second. As he said, all this is a pointer, that is, a pointer to a pointer to a pointer is given a "value" to another pointer. Also there is a memory barrier.


And it’s completely off topic, but according to their code, it’s not at all necessary to answer: why do we need cache_line_size and cache_line_pad , as well as the construction below?

 spsc_queue(spsc_queue const&); spsc_queue& operator = (spsc_queue const&); 
  • I had 2 main answers to my question, what does const_cast <const volatile type*> : either so you can specify which particular qualifier to remove, or, on the contrary, this qualifier attaches to something (does not remove). And, judging by the logic of the code, here is just the second. In load_consume no external content (parameter) changes, which means that this parameter can be made constant. Well and in both cases we, actually, add still volatile . It remains for me to find out what the answer to the last question is. - brenoritvrezorkre

1 answer 1

An interesting code, either I don’t know something about the Intel compiler, or the code was written by a not quite competent programmer. So, in none of these cases is const_cast needed. Enough simple and clear static_cast :

 static_cast <const volatile node*> (param) //это в load_consume static_cast <volatile node*> (param) // это в store_release. 

Now, for what volatile needed: earlier, when the C ++ standard of the year 11 was not approved, volatile was the only cross-platform way for the compiler to read exactly the last value of an object, i.e. no compiler caching happens. Only one detail remains - the processor can and will cache objects, so barriers are in the code (specifically in this version of the code barriers for the compiler, but not for addr , but for other objects, since in x86 CPU barriers are not required for these cases) .

Using volatile + memory barriers achieves guaranteed receipt of the latest values ​​of objects, which is necessary for synchronization.

If you are interested in reading about memory barriers, volatile, and how things are now in C ++, you can read my articles: by barriers , in modern C ++: once and twice


Why cache_line_pad needed: as you can see, the code has a separation into consumer and supplier data, and it is assumed that the data will be used from different streams. But if it turns out that the supplier and consumer data are in the same cache line, then the situation will turn out that the cache line will “spoil” either with one thread or another, because of this there will be constant traffic between the cache blocks of different cores. This will worsen the performance, maybe even much. Therefore, the code ensures that the data that should be used in different threads are in different cache lines.


Why do we need lines:

 spsc_queue(spsc_queue const&); spsc_queue& operator = (spsc_queue const&); 

They are needed in order to prohibit copying of the spsc_queue structure, as you can see, both methods are in private sections and have no implementation, so when you try to copy spsc_queue compiler will spsc_queue an error.

PS next time create separate topics for different questions, and do not fill several non-related questions into one.