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

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

ただいまの
回答率

89.97%

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

解決済

回答 4

投稿

  • 評価
  • クリップ 3
  • VIEW 7,146

raccy

score 18776

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

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

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

int main()
{
    std::atomic_int x(0);
    int y(0);
    std::mutex mw;
    std::mutex mr;
    std::thread thws[1000];
    std::thread thrs[1000];
    for (int i = 0; i < 1000; ++i) {
        thws[i] = std::thread([&]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::lock_guard<std::mutex> lock(mw);
            x++;
            y++;
        });
        thrs[i] = std::thread([&]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            int x_t = x; // ① 安全
            int y_t = y; // ② 危険???
            std::lock_guard<std::mutex> lock(mr);
            std::cout << x_t << "," << y_t << std::endl;
        });
    }
    for (int i = 0; i < 1000; ++i) {
        thws[i].join();
        thrs[i].join();
    }
    std::cout << x << "," << y << std::endl;
    return 0;
}

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

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

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+5

原子的だと思われる型について、変更がある場所を全て排他制御されている場合でも、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/11 06:15

    仕様上は未定義でなんら保証されないって事なのですね。

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

    キャンセル

  • 2016/11/11 11:35 編集

    「mutexそれ自体が重い」というのは単なる盲信の事が多いと思います。mutexロック獲得・解放にかかる処理負荷は実処理に比べれば無視できる程度ですし、もしそうでないならば根本的な設計に重篤な問題を抱えています。

    https://cpplover.blogspot.jp/2012_05_01_archive.html も面白いデータです。当然ながら環境により具体的な数値は異なりますが、数値のオーダーは概ね普遍的でしょう。)

    並行処理においては、ほとんどすべてのケースでmutexの利用を強く推奨します。atomicを用いて**正しい**並行処理を実装するのは非常に困難です。並行処理のデバッグで地獄を見たくなければ、処理速度よりもまずは正しいことが自明なプログラムを書くべきです。

    ここでの正しさとは、「起こり得る全てのスレッドスケジューリング実行において仕様通りの結果が得られること」を意味します。並行処理では事後テストがあまり役に立たないため(無駄ではありませんが網羅性は絶望的です)、理論的な正しさの検証、突き詰めると"証明"するプロセスは必須です。mutexによる排他制御であれば比較的容易(*)に検証できますが、atomic変数を用いたロックフリー(lock-free)処理の検証は非常に難解です。

    *注:あくまでもatomic変数によるロックフリー・アルゴリズムに比べればという比較問題ですけどね :P

    キャンセル

+3

こんにちは。

②の処理は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/11 06:22

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

    キャンセル

  • 2016/11/11 11:03

    > 書き込み部分全体にはmutexは外せなかったんですよ(設計が悪いのかも知れない><)。

    複数の変数をアクセスする場合、mutex保護が一番確実で性能も良いですよ。
    保護が必要な変数って完成するまでの間に意外に増えますし、完成後も仕様変更で追加する必要が出てくることもそれなりにあります。
    更に、無理やり変数を1つにしてもそれがアトミックにアクセスされない型なら結局mutexで保護されますし。

    また、アクセス順序の工夫による保護は非常に難しいです。失敗するとたいへん低い確率で発生するバグになるので、できるかぎり使うべきではありません。まじ難しいです。私は1サイクル1uSec程度の非常に遅いCPUをアセンブラで使っていた時くらいしか使ったことありません。

    つまり、マルチ・スレッドに於ける変数のアクセス保護は、まずはmutexを使うのが良いと思います。そして、一旦完成し、保護が必要な変数が1つだけで、かつ、アトミックにアクセスされることが期待でき、今後変数の追加が考えにくいケースでのみstd::atmicを使うことが望ましいと思います。
    もし、原理的に1変数保護で十分な筈の時には最初からそのように設計できればベストですね。

    キャンセル

  • 2016/11/11 11:48

    Chironianさんコメントに同意です。

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

    キャンセル

  • 2016/11/11 18:52

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

    キャンセル

+3

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

必要です。

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

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

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

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

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

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/11 06:07

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

    キャンセル

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

  • ただいまの回答率 89.97%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる