4

I have code that works with large data blocks having different layouts. The layout will determine which part of the data is fixed, and which data is not fixed. Once data is fixed in a block, it normally doesn't change anymore. So all code reading data will always see the same data.

However, other services may make changes in these blocks as long as they are sure that no code will read that part of the block. To simplify the code, blocks that contain a change will be sent from one service to the other, regardless of the layout of the block. The receiving service will then overwrite the complete block, including the data that was not changed. Let me illustrate this with an example:

Suppose we have the following block of data:

57 23 98 17 25 00 00 00 00 00

And imagine that the first 5 values are 'fixed'. Code in our service will only read the first 5 values and will never read the next 5 values. We can guarantee this due to the design of our architecture. The next 5 values don't really make sense so I put zeroes in the table to illustrate this.

Now another service determines the next 5 values, sends the complete block to our service, and we simply overwrite the complete block with the new data. Since the first 5 values were 'fixed', they remain the same, but the code that transfers and overwrites the block, doesn't know about the layout of the block, so the only thing it can do is overwrite the complete block. This is the result:

57 23 98 17 25 08 33 42 71 85

As said before, the first 5 values did not change, although they were overwritten by the transfer logic.

Question is: Is this a data race? Is it allowed to overwrite a memory address with exactly the same value if other threads can read the data at the same time?

Patrick
  • 23,217
  • 12
  • 67
  • 130
  • 4
    I like your question, think it is clear and wonder whether I can answer..... However, keep in mind that users at StackOverflow **love** to see code. Maybe you can mock up some code which illustrates the activities you describe. Use generous commenting to express implcicits or specialties. – Yunnosch Jun 14 '21 at 09:39
  • I assume that we are looking at normal memory. Nothing like memory-mapped peripherals, or DMA-handled areas, .... Please confirm or elaborate. – Yunnosch Jun 14 '21 at 09:41
  • 1
    @Yunnosch Even if there were such effects – as source and destination are identical anyway, even a spoiled read of a value *cannot* lead to inconsistent data, so unless the standard states this being UB, I don't see a problem with. However the *changed* part needs to be protected. But how should the coping code do so if not knowing which part needs protection? And if it knows, it can simply skip the part without such need... – Aconcagua Jun 14 '21 at 09:49
  • 2
    @Aconcagua You seem to be living/working in a more protected world than I do. If you never encountered hardware were writing to one byte/register changes the value read from another one - then consider yourself happy. I am talking very weird hardware here. and only ask for confirmation because I learned that being paranoid is a healthy state of mind for an (embedded) programmer. – Yunnosch Jun 14 '21 at 09:52
  • @Yunnosch Good point – silently assumed this is ordinary RAM, not noticing that functional registers mapped to memory address space might be involved here as well... – Aconcagua Jun 14 '21 at 09:56
  • By the way: *'it normally doesn't change anymore'* – and what about exceptional situations? – Aconcagua Jun 14 '21 at 09:58
  • @Yunnosch, yes this is about normal memory. Nothing fancy. – Patrick Jun 14 '21 at 10:30
  • @Aconcagua, we can guarantee that the memory does not change. If it will be changed, than we have additional procedures that prevent other threads from reading that data. – Patrick Jun 14 '21 at 10:30
  • Then my original comment should apply again... And again: how are you going to handle the changed values, in your example the next five values? – Aconcagua Jun 14 '21 at 10:36
  • @Aconcagua. The design contains a concept of partitions, where the code that reads the blocks knows about partitions, and knows which parts can be safely read. The data transfer part just transfers blocks, not knowing the internal structure of the blocks. Meta-data of the data transfer then indicates which partitions are now safe to read. Partitions become 'valid-for-change' again, if they are not in use anymore, after which they can be changed by the other service, and transferred using the transfer logic. – Patrick Jun 14 '21 at 10:59
  • Since example is shown like first block is 5 bytes and next blocks continues immediately, then yes most probably this is a race condition. Remember that processor and cache optimization can change order of writes and reads, so data can be visible to other thread in different order. – Marek R Jun 14 '21 at 11:00

2 Answers2

3

Is this a data race?

Yep.

Is it allowed to overwrite a memory address with exactly the same value if other threads can read the data at the same time?

Not explicitly - and that's not the only issue, either.

If your compiler actually performs a single 8-byte load, you have a real (ie, not even potentially just theoretical) data race on the last 3 bytes. Say you have a hypothetical machine where the uint64_t value 57 23 98 17 25 00 00 42 is a trap representation, your update thread used memmove, and it copies the update backwards.

However, a data race means behaviour is undefined by the standard. It may be well-defined on a particular platform - such as any platform which doesn't have integer trap representations, any platform where you know the compiler will really use byte loads, or any platform with explicit semantics for idempotent stores.

See, for example, [intro.races] note 23:

Transformations that introduce a speculative read of a potentially shared memory location might not preserve the semantics of the C++ program as defined in this document, since they potentially introduce a data race. However, they are typically valid in the context of an optimizing compiler that targets a specific machine with well-defined semantics for data races. They would be invalid for a hypothetical machine that is not tolerant of races or provides hardware race detection

(there's no such note for non-speculative races and no specific exception for your idempotent stores, but it's reasonable to take the same approach there IMO).

Obviously if you can write your code so it doesn't depend on these platform details it will be more portable and less fragile in the face of compiler and/or platform updates. Just atomically swapping between two versions of a block (so you read from a copy guaranteed to be unchanging, and write to a copy guaranteed not to be shared) will always be correct, and may even be faster due to reducing cache/coherency traffic.

Useless
  • 64,155
  • 6
  • 88
  • 132
0

There are so many "it depends" that this question cannot be answered with a clearstatement.

If this construct is part of some productive code, then the answer is: Definitely yes. A race condition may occur. Task synchonization mechanisms must be used. There is no discussion possible.

A M
  • 14,694
  • 5
  • 19
  • 44