sizeof(size_t)未満の幅の型の、メモリ上隣り合った変数に関して、マルチスレッドプログラムはデータ競合なく読み書きできますか？


私は、複数のスレッドから以下のArrayの要素にアクセスすることを考えました。  
加えて、インデックスをatomicに管理することで、複数のスレッドから同時に同じ要素には触らないという条件を設けました。

[code]

    std::array<uint8_t, SIZE> ar;
    std::array<Struct_70bit, SIZE> ar; //あるいはsizeof(size_t)の幅で区切ることが出来ない型
    
[/code]

この場合、隣り合った要素はスレッドセーフに読み書きできますか？

もしスレッドセーフでない場合の対策として、私は以下のようにすることを考えましたが、これは正しいですか？

[code]

    alignas(size_t) std::array<uint8_t, SIZE> ar;
    alignas(size_t) std::array<Struct_70bit, SIZE> ar;
    
[/code]

P.S.  
スレッドセーフの定義が曖昧だとご指摘いただきました。  
私自身がスレッドセーフという言葉を取り違えていました。お詫びします。  
ここではさらに具体的に、「特別な管理によって配列の同じインデックスにアクセスすることはないが、小さな型のとき隣り合った要素アクセスにはデータ競合が生じるか（アクセスした要素の周囲の要素が、意図しない値になるか）」という点についての質問でした。




スレッドセーフ / atomic という用語の解釈次第なところがありますが、この手の話が問題になるのはメインメモリのほかにキャッシュを持つマルチコア CPU
(ないしは SMP 構成) の場合に限定できるでしょう。

アトミックという用語は「その変数の１回の読み書きが他者に阻害されない」という意味でしかありません。よって、マルチコア/SMP 構成で、複数のコア / CPU
が複数の変数を連続的に読み書きする場合に「順番が入れ替わったように観測される」ことによるスレッド安全性までは担保されません。

[メモリバリア](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%90%E3%83%AA%E3%82%A2)
で説明されている、最近の高性能 CPU が持つ「アウトオブオーダー」な実行に対して、

  * それでもよい

ならばおそらく提示されたソースで十分です (x86 / x64 はこちらの状況にはならないと推測でき、検証するなら他の CPU で試す必要がある)

  * それはスレッドセーフでない

なら真にメモリバリアが必要で、その場合 `alignas` ではおそらくダメで `std::atomic` な指定が必要になると思われます。

検証例１

[code]

    std::array<uint8_t, SIZE> a1;
    alignas(size_t) std::array<uint8_t, SIZE> a2;
    
[/code]

に対して

  * cygwin64 の gcc-8.2.0 / 9.3.0 は `movb` を生成
  * s2019 の cl.exe version 19.26.28806 for x64 は `mov byte ptr` を生成

メモリバリアな命令を生成していない (高速)

検証例２

[code]

    std::array<std::atomic<uint8_t>, SIZE> ar3;
    
[/code]

  * cygwin64 の gcc-8.2.0 / gcc-9.3.0 は `movb` のほかに `mfence` 命令を追加生成
  * vs2019 の cl.exe version 19.26.28806 for x64 は `mov` の代わりに `xchg` 命令を生成

メモリバリアな命令が生成されている (超絶遅い)

ということで `std::atomic`
にすると安全かもしれませんが超絶遅くなります。なので「スレッドセーフ」の意味をきっちり定義してから始めたほうがよさそうです。

* * *

この話は純粋にハードウエアレベルのことなので、コンパイラが [c](/questions/tagged/c "'c' のタグが付いた質問を表示") だろうと
[c++](/questions/tagged/c%2b%2b "'c++' のタグが付いた質問を表示") だろうと
[golang](/questions/tagged/golang "'golang' のタグが付いた質問を表示")
だろうとあまり違いは無くてどの言語を使っても問題は発生します。

* * *

32bit CPU で同一データバス 32bit 内の違う 8bit に異なるスレッドから「単純 write
」を行ってもハードウエア上データは壊れません（そのように CPU は作ってあります)
安心してください。ただし「アトミックである型」でアクセスしたとき限定です。

[code]

    struct test_type {
        unsigned char a;
        unsigned int x:4;
        unsigned int y:8;
        unsigned int z:4;
    };
    
[/code]

のようなものを作ったとしたら、おそらくは `a` はアトミックですが `y` はアトミックであることを期待してはいけません
(アトミックになるかもしれないしならないかもしれない）

また原理的に read-modify-write になるアクションはアトミック操作になりえませんので注意。 `ar[i]++;` のような書き換えは
`ar[i]` が `char` 型であってもアトミック操作ではありません。時と場合によっては処理系が `InterlockedIncrement`
みたいな API を用意していますので、これを使うしかないです。




スレッドセーフ / atomic という用語の解釈次第なところがありますが、この手の話が問題になるのはメインメモリのほかにキャッシュを持つマルチコア CPU
(ないしは SMP 構成) の場合に限定できるでしょう。

アトミックという用語は「その変数の１回の読み書きが他者に阻害されない」という意味でしかありません。よって、マルチコア/SMP 構成で、複数のコア / CPU
が複数の変数を連続的に読み書きする場合に「順番が入れ替わったように観測される」ことによるスレッド安全性までは担保されません。

[メモリバリア](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%90%E3%83%AA%E3%82%A2)
で説明されている、最近の高性能 CPU が持つ「アウトオブオーダー」な実行に対して、

  * それでもよい

ならばおそらく提示されたソースで十分です (x86 / x64 はこちらの状況にはならないと推測でき、検証するなら他の CPU で試す必要がある)

  * それはスレッドセーフでない

なら真にメモリバリアが必要で、その場合 `alignas` ではおそらくダメで `std::atomic` な指定が必要になると思われます。

検証例１

[code]

    std::array<uint8_t, SIZE> a1;
    alignas(size_t) std::array<uint8_t, SIZE> a2;
    
[/code]

に対して

  * cygwin64 の gcc-8.2.0 / 9.3.0 は `movb` を生成
  * s2019 の cl.exe version 19.26.28806 for x64 は `mov byte ptr` を生成

メモリバリアな命令を生成していない (高速)

検証例２

[code]

    std::array<std::atomic<uint8_t>, SIZE> ar3;
    
[/code]

  * cygwin64 の gcc-8.2.0 / gcc-9.3.0 は `movb` のほかに `mfence` 命令を追加生成
  * vs2019 の cl.exe version 19.26.28806 for x64 は `mov` の代わりに `xchg` 命令を生成

メモリバリアな命令が生成されている (超絶遅い)

ということで `std::atomic`
にすると安全かもしれませんが超絶遅くなります。なので「スレッドセーフ」の意味をきっちり定義してから始めたほうがよさそうです。

* * *

この話は純粋にハードウエアレベルのことなので、コンパイラが [c](/questions/tagged/c "'c' のタグが付いた質問を表示") だろうと
[c++](/questions/tagged/c%2b%2b "'c++' のタグが付いた質問を表示") だろうと
[golang](/questions/tagged/golang "'golang' のタグが付いた質問を表示")
だろうとあまり違いは無くてどの言語を使っても問題は発生します。

* * *

32bit CPU で同一データバス 32bit 内の違う 8bit に異なるスレッドから「単純 write
」を行ってもハードウエア上データは壊れません（そのように CPU は作ってあります)
安心してください。ただし「アトミックである型」でアクセスしたとき限定です。

[code]

    struct test_type {
        unsigned char a;
        unsigned int x:4;
        unsigned int y:8;
        unsigned int z:4;
    };
    
[/code]

のようなものを作ったとしたら、おそらくは `a` はアトミックですが `y` はアトミックであることを期待してはいけません
(アトミックになるかもしれないしならないかもしれない）

また原理的に read-modify-write になるアクションはアトミック操作になりえませんので注意。 `ar[i]++;` のような書き換えは
`ar[i]` が `char` 型であってもアトミック操作ではありません。時と場合によっては処理系が `InterlockedIncrement`
みたいな API を用意していますので、これを使うしかないです。

