質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.37%
C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

5回答

15891閲覧

変更部分が全て排他制御されている場合でもatomicは必要か?

raccy

総合スコア21737

C++11

C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

2グッド

3クリップ

投稿2016/11/10 13:41

intboolのような読み込みが原子的だと思われる型について、変更がある場所を全て排他制御されている場合でも、atomicを使う必要はあるのでしょうか?

サンプルとして、下記のコードを見てください。

C++

1#include <atomic> 2#include <chrono> 3#include <iostream> 4#include <thread> 5 6int main() 7{ 8 std::atomic_int x(0); 9 int y(0); 10 std::mutex mw; 11 std::mutex mr; 12 std::thread thws[1000]; 13 std::thread thrs[1000]; 14 for (int i = 0; i < 1000; ++i) { 15 thws[i] = std::thread([&]() { 16 std::this_thread::sleep_for(std::chrono::seconds(1)); 17 std::lock_guard<std::mutex> lock(mw); 18 x++; 19 y++; 20 }); 21 thrs[i] = std::thread([&]() { 22 std::this_thread::sleep_for(std::chrono::seconds(1)); 23 int x_t = x; // ① 安全 24 int y_t = y; // ② 危険??? 25 std::lock_guard<std::mutex> lock(mr); 26 std::cout << x_t << "," << y_t << std::endl; 27 }); 28 } 29 for (int i = 0; i < 1000; ++i) { 30 thws[i].join(); 31 thrs[i].join(); 32 } 33 std::cout << x << "," << y << std::endl; 34 return 0; 35}

xは原子的intですがyは普通のintです。しかし、インクリメントの処理について、mutexによりどちらも排他制御されているため、期待通りに最終的に1000になります(もし、mutexがなければ、yは1000で無いときがあります)。

では、読み込み側はどうでしょうか?①は安全でしょう。x++の最中にxが読み込まれて壊れてしまうことはありません。では②はどうなのでしょうか?y++の最中にyが読み込まれた場合、おかしな値になることはあるのでしょうか?また、後置インクリメント以外、例えば、前置インクリメントや代入、+=などの場合は、うまくいかないとかあるのでしょうか?そして、それらのことはint以外の、boollong long等の他の整数型についても言えることなのでしょうか?(とりあえず、上のコードを手元で試す限り、yがおかしな値を返すことはないようです。)

もし、問題が無ければ、atomicを外して僅かでも処理を速くしたいと考えています(排他制御自体は別の理由で外せないことが前提です)。変更が全て排他制御されていれば、atomicは外しても大丈夫なのかどうかを教えてください。

moonphase, ikuwow👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

ベストアンサー

原子的だと思われる型について、変更がある場所を全て排他制御されている場合でも、atomicを使う必要はあるのでしょうか?

C++言語仕様の観点からは「atomicは必須です」。atomicでない変数に関して、同時にRead/Writeが行われると 未定義動作(Undefined Behavior) です。

変更が全て排他制御されていれば、atomicは外しても大丈夫なのかどうかを教えてください。

上記理由により「NG」です。変更と読込全てを排他制御するか、素直にatomic変数を利用してください。


では②はどうなのでしょうか?y++の最中にyが読み込まれた場合、おかしな値になることはあるのでしょうか?

C++言語仕様としては何の保証もありませんから、「おかしな値になる」ことも含めて何が起きても不思議ではありません。

(とりあえず、上のコードを手元で試す限り、yがおかしな値を返すことはないようです。)

現実のC++コンパイラとプロセッサアーキテクチャの下では、大抵のケースでプログラマの期待通り振舞うと予想されます。しかし、繰り返しになりますが仕様上は一切の保証がなくなりますので、理論上は壊れているプログラムです。

(「目の前のコードが動いているから良いのだ」という態度はそれはそれでアリです。あくまで自己責任で。)


atomicを外して僅かでも処理を速くしたいと考えています(排他制御自体は別の理由で外せないことが前提です)

正直言っておすすめはできませんが、atomic変数の操作にmemory orderを指定することで処理の高速化を図れる可能性があります。

ただし、以下の条件付きです:

  • C++言語のメモリモデルを理解していること。少なくとも、relaxed/release-acquire/sequential consistencyメモリアクセスの違いを正しく認識していないと、プログラムに超難解な潜在バグを埋め込むだけです。
  • 対象アーキテクチャのハードウェアメモリモデルを十分に理解していること。ARMアーキテクチャなど弱いメモリモデルでは大きな効果を得られる可能性がありますが、x86アーキテクチャなど強いメモリモデルでは期待するほどの効果は得られないでしょう。

また、対象プログラムにおいてatomicアクセスが真の問題になるほど重いかは十分考慮してください。(個人的には)そもそも並列化設計が適切でないことがほとんどと思います。その変数は本当に並行更新する必要があるのでしょうか?Map-Reduce方式のような更新はできないのでしょうか?

下記記事もご参考に:

投稿2016/11/10 14:21

編集2016/11/10 14:29
yohhoy

総合スコア6191

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

raccy

2016/11/10 21:15

仕様上は未定義でなんら保証されないって事なのですね。 処理は僅かでも軽くせよ…みたいなプレシャーがC++を書いているとどこからともなくかかってくるので、atomic外せないかなーってことでこの質問をしてみました。まだ、テスト段階のプログラムなので、処理がやたら重いとかありましたら、設計自体を見直したいと思います。
yohhoy

2016/11/11 02:45 編集

「mutexそれ自体が重い」というのは単なる盲信の事が多いと思います。mutexロック獲得・解放にかかる処理負荷は実処理に比べれば無視できる程度ですし、もしそうでないならば根本的な設計に重篤な問題を抱えています。 ( https://cpplover.blogspot.jp/2012_05_01_archive.html も面白いデータです。当然ながら環境により具体的な数値は異なりますが、数値のオーダーは概ね普遍的でしょう。) 並行処理においては、ほとんどすべてのケースでmutexの利用を強く推奨します。atomicを用いて**正しい**並行処理を実装するのは非常に困難です。並行処理のデバッグで地獄を見たくなければ、処理速度よりもまずは正しいことが自明なプログラムを書くべきです。 ここでの正しさとは、「起こり得る全てのスレッドスケジューリング実行において仕様通りの結果が得られること」を意味します。並行処理では事後テストがあまり役に立たないため(無駄ではありませんが網羅性は絶望的です)、理論的な正しさの検証、突き詰めると"証明"するプロセスは必須です。mutexによる排他制御であれば比較的容易(*)に検証できますが、atomic変数を用いたロックフリー(lock-free)処理の検証は非常に難解です。 *注:あくまでもatomic変数によるロックフリー・アルゴリズムに比べればという比較問題ですけどね :P
guest

0

intやboolのような読み込みが原子的だと思われる型について、変更がある場所を全て排他制御されている場合でも、atomicを使う必要はあるのでしょうか?

必要です。

たとえCPUがその変数をアトミックに読み書きできるとしても、コンパイラーの最適化により、意図したタイミングで読んでくれないかもしれません。そういう場合、以前はvolatileを使っていた時期もありましたが、volatileはマルチスレッドを想定していないのでC++11以降はatomicを使うべきでしょう。

投稿2016/11/10 21:06

catsforepaw

総合スコア5944

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

こんにちは。

②の処理はmutexで保護されていません。
なので、もし、int型がアトミックでないような処理系の場合は不具合がいつかでます。
ただ、int型がアトミックでない処理系が存在する可能性はかなり低いと思います。
int型は通常そのCPUが最もアクセスするのが得意な型なので、それはアトミックに(1回のパスサイクルで)アクセスされます。

次にmutex保護とstd::atmicによる保護を多重にかける必要はないです。
std::atmicの実装を見たことはないですが、intなら元々アトミックなCPUが多いので、そのようなCPUなら保護をわざわざかけていないことを期待できます。それを期待する場合はmw保護を外してyをstd::atmic_intにするのが好ましいと思います。
逆にそれを期待しないのならば、mutexによる保護の回数を減らすために、全部1つのmutexで保護してしまい、std::atmic_intしないのが好ましいと思います。


【補足】
書き方がちょっとまずかったようですので補足します。

まず、マルチ・スレッド・プログラムで書き込みを行う場合がある変数は、書き込みと読み出しの両方ともmutexなりatmicなりで保護するべきです。

さて、std::atmicでどのようにして「アトミック」にアクセスできるよう保護されているのか、考えておく必要があります。複数回に分けてアクセスされるメモリを確定的に保護する場合、mutex等による排他制御が必要です。そして、一般にmutexが最も軽いと言われています。なのでstd::atmicの実装もmutexを使っている筈です。つまり、どちらを使っても性能的には大差ないです。

ただし、元々アトミックにアクセスされる型なら改めてmutexにより保護する必要はないですから、処理系はmutex保護を省略する実装になっている筈です。なので効率は上がります。(そのためのstd::atmicと思います。でなければ毎回mutexで保護する効率の悪いstd::atmicなど必要性は低いですから。)

しかし、同時に複数の変数をstd::atmicで保護した場合、それがアトミックにアクセスされない型なら、それぞれが別途mutex等の保護下にはいるため、効率は落ちます。
慎重に見極めないと性能は劣化します。

そして、int型はアトミックにアクセスされる処理系がほとんどでしょうから、std::atmic_intは積極的に使っていってよいと思います。

投稿2016/11/10 14:09

編集2016/11/11 04:20
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

raccy

2016/11/10 21:22

mutex自体外したいのはあったんですが、今書いているコードが、処理の関係上、書き込み部分全体にはmutexは外せなかったんですよ(設計が悪いのかも知れない><)。書き込みはたまにしか無いが、読み込み頻繁にあって、読み込みがある処理の全体はmutexの必要が無いって設計だったのです。書き込みの処理全体のmutexを外せないかは検討したいと思います。
Chironian

2016/11/11 02:03

> 書き込み部分全体にはmutexは外せなかったんですよ(設計が悪いのかも知れない><)。 複数の変数をアクセスする場合、mutex保護が一番確実で性能も良いですよ。 保護が必要な変数って完成するまでの間に意外に増えますし、完成後も仕様変更で追加する必要が出てくることもそれなりにあります。 更に、無理やり変数を1つにしてもそれがアトミックにアクセスされない型なら結局mutexで保護されますし。 また、アクセス順序の工夫による保護は非常に難しいです。失敗するとたいへん低い確率で発生するバグになるので、できるかぎり使うべきではありません。まじ難しいです。私は1サイクル1uSec程度の非常に遅いCPUをアセンブラで使っていた時くらいしか使ったことありません。 つまり、マルチ・スレッドに於ける変数のアクセス保護は、まずはmutexを使うのが良いと思います。そして、一旦完成し、保護が必要な変数が1つだけで、かつ、アトミックにアクセスされることが期待でき、今後変数の追加が考えにくいケースでのみstd::atmicを使うことが望ましいと思います。 もし、原理的に1変数保護で十分な筈の時には最初からそのように設計できればベストですね。
yohhoy

2016/11/11 02:48

Chironianさんコメントに同意です。 補足ですが、atomic変数アクセスが意図通りのCPU命令(CPU視点でアトミックなメモリアクセス命令)に落ちるか否かは、std::is_lock_free関数やATOMIC_*_LOCK_FREEマクロで確認できますね。
raccy

2016/11/11 09:52

ふむふむ、なかなか難しい…。読み込み側もmutexがいいのか見直してみます。
guest

0

コアな質問ですね。
そもそも atomic性を保証する方法の一つとして、
Mutexがあります。ただ重い。

なのでもっと軽く、実現する方法の一つとして、
std::atomic_int の様な手法があります。

それから、おそらく高速化のため「原子的だと思われる型」に
対してほっといても、atomic なアクセスになるのでは?
的な期待をしているように、文面から思えます。

実際のところ、CPUのアーキテクチャとコンパイラの出力に
依存します。

なので、C言語で書いたコードだけでは判断できません。
自分も、コンパイラーにアセンブルリストを出力させて、
その出力結果を見て、排他制御が必要か判断しています。
でも、シングルコアの場合です。

ただ、マルチコアになると、アセンブラの
1インストラクションだからといっても、並列動作する
ので、安全が保障されない事がありえます。

コア1がコア1のキャッシュに結果を書いて、
コア2が同じ変数をコア2のキャッシュから読んで、
演算して書き出す。これが同時に走ると、
ダメですよね。

現状シングルコアでも、書いたコードが将来、
マルチコアで実行される可能性もあるかと思いますし、
CPUのアーキテクチャも日々変わっていくので、
安全なのは、Mutexなり、atomic 宣言などして、
明確にコンパイラに意図を伝える事です。

投稿2016/11/10 14:31

ShinyaAnan

総合スコア241

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

raccy

2016/11/10 21:07

ぬぬ、アセンブリレベルで確認して、なおかつマルチコアも考慮しないと保証はできないと…。それなら、もうatomicを正直に使った方がいいですね。
guest

0

これ別に問題ないですよね。
2つのスレッド処理のうち前者は同期を行ったうえでRead/Writeを行いますが、
後者はReadだけですので少なくとも未定義動作にはなりません。

そして意図した動作が得られるかですが、後者のスレッドがただ変数の現在値を読み取って
記録したりするだけで、前者のスレッドと同期したいわけではないのなら
(つまり0,0 1,1 2,2... と漏れなく前者の動作と同期したいわけではないのなら)
全く問題ありません。

atomicは無くても大丈夫です。

投稿2021/02/02 15:57

tanakanata

総合スコア4

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.37%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問