67978


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


CC BY-SA 4.0
2020-06-24T22:36:15.000
67969
2020-06-27T00:36:11.120
2020-06-26T07:44:02.743
37013
37013
question
c++
sizeof(size_t)未満の幅の型の、メモリ上隣り合った変数に関して、マルチスレッドプログラムはデータ競合なく読み書きできますか？


スレッドセーフ / 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 を用意していますので、これを使うしかないです。


CC BY-SA 4.0
2020-06-25T03:25:28.583
67978
2020-06-26T03:59:52.073
2020-06-26T03:59:52.073
8589
8589
67969
answer


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

はい、できます。C++言語仕様は、異なるスレッドから **異なる変数** に対してそれぞれ同時アクセスしても安全であることを保障します。

>
> 特別な管理によって配列の同じインデックスにアクセスすることはないが、小さな型のとき隣り合った要素アクセスにはデータ競合が生じるか（アクセスした要素の周囲の要素が、意図しない値になるか）

前述の通り、データ競合は発生しません。最もサイズが小さい`char`型の配列であっても、下記コードは安全に実行されます。

[code]

    char arr[2] = {1, 2};
    
    // スレッド1
    arr[0] = 100;
    
    // スレッド2
    int r = arr[1];
    
[/code]

* * *

C++言語としてはプログラムが安全に動作することを保障しますが、現実的にはプログラムは正常動作するものの速度ペナルティを受けることがあります。現代のプロセッサはメモリキャッシュ(Memory
cache)機構を備えており、この速度ペナルティはキャッシュ管理単位「キャッシュライン(Cache line)」に起因します。

例えば`char`型配列の隣接要素はC++言語としては異なる変数ですが、プロセッサはこれらを同一キャッシュラインとして扱います。この状況を「False
Sharing（偽共有）」と呼びます。

False Sharing
が起きている状況で、異なるスレッドから書込(`arr[0]=100`)と読込(`r=arr[1]`)を行うと同一キャッシュラインに対する読み書き競合が発生し、プログラム実行速度が低下するという事象が発生します。あくまでも処理が遅くなるだけで、プログラムは正しく動作します。

C++17からは False Sharing を回避する、つまり隣接する変数のメモリアドレスを意図的に離す仕組みを提供します。

[code]

    // 通常は同一構造体内の2個のatomic<int>変数は隣接メモリアドレスに配置される。
    // alignas(～)により2個の変数アドレスがキャッシュラインサイズの倍数に整列、
    // つまり必ず異なるキャッシュラインに載るようメモリ位置を離すよう指示する。
    struct keep_apart {
      alignas(std::hardware_destructive_interference_size) std::atomic<int> cat;
      alignas(std::hardware_destructive_interference_size) std::atomic<int> dog;
    };
    
[/code]

  * <https://en.cppreference.com/w/cpp/thread/hardware_destructive_interference_size>
  * <https://cpprefjp.github.io/reference/new/hardware_destructive_interference_size.html>


CC BY-SA 4.0
2020-06-26T08:56:08.710
68028
2020-06-26T09:16:33.443
2020-06-26T09:16:33.443
49
49
67969
answer


参考までに、[バイト単位のロード・ストア命令を持たないプロセッサアーキテクチャ](https://ja.wikipedia.org/wiki/DEC_Alpha#%E8%A8%AD%E8%A8%88%E5%8E%9F%E5%89%87)も存在します。この場合、１バイト書き込みを実現するためには

  1. ４バイト読み込みし
  2. 希望する位置のバイトを書き換え
  3. ４バイト書き込み

という手順を踏むため、隣り合った要素はスレッドセーフに書き込みできません（たぶん）。


CC BY-SA 4.0
2020-06-27T00:36:11.120
68047
2020-06-27T00:36:11.120
4236
67969
answer