C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[9]: 変数のアトミック性を保障するコーディング方法について


(過去ログ 111 を表示中)

[トピック内 11 記事 (1 - 11 表示)]  << 0 >>

■65737 / inTopicNo.1)  変数のアトミック性を保障するコーディング方法について
  
□投稿者/ howling (244回)-(2013/03/15(Fri) 14:08:48)

分類:[.NET 全般] 

別スレから乗っ取っちゃってたので移動&質問のみ修正。

マルチスレッドの上では変数のアトミック性を保障するために、

1.ロック取得
2.変数に代入or変数から取得
3.ロック解除

のようなコーディングをしそうな気がします。
軽く調べてみたのですが、大体変数のアトミック化は保障されているようですね。
でも、longは保障されていない?とこちらの記事にはありました。

http://d.hatena.ne.jp/siokoshou/20091003

C++で同じようなコーディングをしていたのは無駄だったのでしょうか…
それとも、C#ではそのようなコーディングをしなくても良いというだけですか?
引用返信 編集キー/
■65738 / inTopicNo.2)  Re[1]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (245回)-(2013/03/15(Fri) 14:09:23)
774RRさんの発言:

普通に 32bit CPU で 32bit 変数を扱う場合、読み込み操作と書込み操作は atomic だ。
(適切に align されている場合に限定)

・ VC++ において long は 32bit だから long x; があるとして
・ C# の int は 32bit だから int x; があるとして
x=1; で行われる書込み操作は atomic
std::cout << x; / Console.WriteLine(x); で行われる読み込み操作は atomic
機械語レベルで1命令なので (mov [x], eax など)

++x; では 読み込み→加工→書き込み なので non atomic
x を複数スレッドで同時アクセスしているとかならロック操作が必要
だから InterlockedIncrement みたいな機能が用意されている。

y=x; では x を読み込む→別スレッドで x が更新される→自スレッドで y を書き込む
なので 全操作は atomic だが y が更新されるとき x の値はもう違うかもしれない

32bit CPU で 64bit 変数を扱う場合は単純読み込み、単純書き込みに見えても non-atomic
VC++ の long long や C# の long は 64bit だから単純操作も non-atomic
なので 64bit 変数の atomic 性質を 32bit 環境で保証したかったらロックが必要
# スレッド間で 64bit 変数を共有しないほうがいい、ってことだ

機械語レベルで複数命令なので
mov [x].hi, 0
ここで他スレッド等が x に干渉
mov [x].lo, 1

C/C++ のビットフィールドも Read/Modify/Write になるので non-atomic


引用返信 編集キー/
■65739 / inTopicNo.3)  Re[2]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (246回)-(2013/03/15(Fri) 14:09:52)
howlingの発言:

(774RR さん) に返信
なるほど…しかし、それだと書き方として、
ここは64bit変数だから…ここは32bitだから…とやっていくのは抜けもありそうで怖いですね。
→だからあぁいった書き方になったのかもしれないですが、経緯の詳細はさっぱりわかってなかった

基本、何か変数を扱う用のロックを1つ作成して持っておき、
それを変数使う処理で使いまわす…なんていう形になってました。

本来は変数ごとにロックを1つ作成すべきなのかもしれないですが…それもそれでどうなんでしょう。
こうやるといいよ、といった推奨の方法などはありますか?
一般解として、でも良いのですが。今後の参考にさせてください。

引用返信 編集キー/
■65740 / inTopicNo.4)  Re[3]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (247回)-(2013/03/15(Fri) 14:10:26)
774RRさんの発言:

スレ乗っ取り? ExecutionEngineException とはあまり関係ない話になってきたが。

まずリンク先で示してある C# 言語仕様書の解釈はおkなんだろうか?
先のコメントで書いたのと同じことが書いてあるだけ。
・特定の型は「読み込みだけ」「書き込みだけ」は atomic
・そうでない型は「読み込みだけ」「書き込みだけ」でも atomic ぢゃない
・結果的に Read Modify Write になる処理は前者の型を使っても atomic ぢゃない
・特別に設計されたライブラリ関数には RmodW を atomic に行えるものがある
ここまでは単なる一般論で、
その「特定の型」が C# の仕様として列挙されている。

一般解としては
0.その処理が当該 CPU で原理的に atomic なのかそうでないのかを理解する
原理的に atomic な処理はロック不要。
# まあこれが簡単にできないから以下が必要なわけだけど

1.変数(の更新)にロックが必要か否かを判断する。
extern int val[];
int calc_sum(void) {
 int sum=0;
 for (int i=0; i<N; ++i) sum+=val[i];
 return sum;
}
のようなとき、たぶん
i の更新 ++i にロックは必要ない。
sum+= の更新にもロックは必要ない。
val はここでは更新されないが、ロックが必要かもしれない(いらないかもしれない)

およその判断基準は
・スレッド間で共有する変数か
・割り込みの中で更新・読み取られる変数か
こういう判断は人間にしかできない。

その上で
・その CPU にとって最適なサイズより大きい変数 (C でいう int より大きい変数:配列を含む)
・ビットフィールド
は「原理的に atomic でない処理」になる可能性が極めて高いので赤信号。
必要があれば生成されるアセンブラコードを見てでも検討する。

最終的にロックが必要な変数って、数は少ないはず。

2.ロックが必要な場合でも代替手段がある可能性がある。
++x; にロックが必要だと判断したら InterlockedIncrement(&x); に置き換えれば、ロック不要
組み込み系の CPU ではこの手の要望に応えるため専用命令が用意されていたりするので
・そもそも bitfield_variable.bit1=0; をロックせずにできる可能性がある
・ビットフィールド専用インラインアセンブラ・ライブラリ関数に置き換える
などの検討を行う。

3.真にロックが必要なら、変数1個で1ロックと考えず、論理的にロックする。
tail と head を不可分に更新するのに tail で1ロック head で1ロックなんてありえない。

ってとこだろうか。
引用返信 編集キー/
■65741 / inTopicNo.5)  Re[4]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (248回)-(2013/03/15(Fri) 14:20:49)
774RRさんへ

仕様書の解釈自体はOKだと思います。
774RRさんのまとめと、解釈は一致していますので。

さて、一般解についてですが…。
やっぱりしっかり理解して、できる限り減らすのが正しいんですね。
とはいえ、私が理解できていない人だったからこそ、「とりあえずロックしとけ」といった形の
コーディングを教えられたのかもしれないです。

特に、2のロックが必要な場合でも代替手段は全く知らなかったです。
>だから全部ロック通すようなテンプレ関数使ってました…

論理的にロックというのは、
ほとんどが関数の開始でロックして、変数の取得などを全部やって解除して、残りの処理…としていたので
これ自体は理に叶っているのかなと。

うーん、結局のところ、問題は0の仕様をしっかり理解できているかどうかなんですね…。
コーディングする時に気を遣わないといけないですね。(遣ってはいるつもりですが)
引用返信 編集キー/
■65791 / inTopicNo.6)  Re[5]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ 774RR (65回)-(2013/03/18(Mon) 17:59:31)
実際のところマルチスレッドでスレッド間共有する変数(特に排他制御に使う変数)は
・変数アクセスの「アトミック性」だけでは足らなくて
・ out-of-order 実行に対する「メモリバリア」の考察も必要となる

# 少なくとも C++ では要考察
# VB/C# でどうなのかは知らない→詳しい人フォロー願う

うまくまとまっていると思うのでリンクなど
http://i-saint.hatenablog.com/entry/20101005/1286208402

引用返信 編集キー/
■65796 / inTopicNo.7)  Re[6]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (252回)-(2013/03/18(Mon) 19:45:58)
No65791 (774RR さん) に返信

はー…。
いつもCriticalSectionをよく用いていたのですが、
なんとなくこういった物でくくっていれば最適化は起きないはず
(というか、マルチスレッドで下手に最適化されてしまうと、おかしなタイミングで触れられる危険性がある)
という勝手な認識でいたのですが、それ自体は正しかったんですね。

とはいえ、volatile接続子の話も聞いたことあるなぁ…と思っていたのですが、
C++の最適化で調べると、実はこればかり出てくるんですね。
私の場合はよくCriticalSectionに触れていたので、この方法が多い!と勝手に思っていたのですが、
それに救われているとは知らず…。

volatile接続子に振り回されず、やっていきたいと思います。
有用な話をありがとうございます。

//しかし、CSとMutexは、何を基準にしてその使用を区別しているのか知りません。ちょっと調べてみます。
引用返信 編集キー/
■65812 / inTopicNo.8)  Re[6]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ yoh2 (1回)-(2013/03/19(Tue) 04:21:26)
横から失礼。
リンク先を読んだところ、out-of-order実行というものが、コンパイラの最適化による
命令順序入れ替えのことのように読めてしまい、紛らわしく感じたのでちょっと補足させて下さい。

out-of-order実行というのは、コンパイラによる最適化とは別の話で、CPUが機械語の
並び順を守らずに実行するというものです。
並び順を守らないといっても、基本的に守った場合と同じ結果になるように因果関係が考慮されて
実行されるのですが、メモリアクセス順はある程度の入れ替えが許容されている場合がある
(詳細はCPUによって異なる) というのが厄介な点ですね。
特にマルチコアだと、キャッシュの都合もあって、それぞれのコアから見たメモリの内容の
更新順序が異なっているというとんでもない状況もザラに。
で、その辺の不整合を解消するために「メモリバリア」を必要に応じて使う必要があります、と。
# そして使いすぎると、メモリ同期の待ちが増えて一気にパフォーマンスが落ちる……

この辺が参考になるでしょうか。
http://www.nminoru.jp/~nminoru/diary/2006/08.html#20060811p1
引用返信 編集キー/
■65877 / inTopicNo.9)  Re[7]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (254回)-(2013/03/21(Thu) 16:38:29)
No65812 (yoh2 さん) に返信
スレ埋もれてた…返信遅れてしまい、申し訳ありません。

> リンク先を読んだところ、out-of-order実行というものが、コンパイラの最適化による
> 命令順序入れ替えのことのように読めてしまい、紛らわしく感じたのでちょっと補足させて下さい。

てっきり最適化で入れ替えが行われているのかと、もろに勘違いしてました。
うーん、ここらへんになってくると、そういう物もあるのか…くらいな認識でいた方が良いのでしょうか。
実際に動かしてみて、デバッグする際に「この変数おかしいぞ?」となると思うのですが、
その可能性の1つとして、こういった物がある、という程度という意味です。

まずは自分のコードを疑うことが第一だと思いますからね…(だからこそ時間を取られてしまうこともありますが)
ひとまず、URLの方は全て拝見しました。
volatileってそんなに古い物だったのか…とかなんとか。
引用返信 編集キー/
■65894 / inTopicNo.10)  Re[8]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ yoh2 (2回)-(2013/03/22(Fri) 02:49:22)
No65877 (howling さん) に返信

> てっきり最適化で入れ替えが行われているのかと、もろに勘違いしてました。
> うーん、ここらへんになってくると、そういう物もあるのか…くらいな認識でいた方が良いのでしょうか。
> 実際に動かしてみて、デバッグする際に「この変数おかしいぞ?」となると思うのですが、
> その可能性の1つとして、こういった物がある、という程度という意味です。

同期を取るのにロックを必ず使うならば雑学程度の扱いでもよいかと思いますが、
アトミック操作をフルに使ってロックフリーにしようと思うならどういうときにどう
おかしくなるかを把握しておかなければハマると思います。

C#で、メモリ操作の順が期待と異なってしまう例を書いてみました。あまり自信ありませんが。

Windows 7 x64, Visual Studio 2012, .Net Framework 4.5, Any CPU,
実CPU: Opteron 6376 で、UseBarrier = falseにすると
[何かヘン!] のコードが実行され、UseBarrier = trueにすると実行されない
(数分間チェックしただけですが)ことを確認できました。
# C#でvolatile使ったことがないので使い方を間違えてるかも。

    public class Program
    {
        // ふたつのスレッドのロックに使うフラグたち。
        private static volatile bool[]  Locks = { false, false };
        // ごく一瞬を除いてほとんどの期間 -1。
        private static volatile int Value = -1;
        // true: メモリバリアを使う。false: メモリバリアを使わない。
        private static bool UseBarrier = false;

        public static void Main(string[] args)
        {
            List<Thread> threads = new List<Thread>();
            for (int i = 0; i < 2; i++)
            {
                // 引数 = 0でThreadBodyを実行するスレッドと
                // 引数 = 1でThreadBodyを実行するスレッドを
                // ひとつずつ作る。
                Thread thread = new Thread(ThreadBody);
                thread.Start(i);
                threads.Add(thread);
            }

            foreach (Thread thread in threads)
            {
                // ここで待ってても終わらないので確認が終わったら強制終了して下さい。
                thread.Join();
            }
        }

        private static void ThreadBody(object id)
        {
            Console.WriteLine("Thread #{0} started.", id);
            for (; ; )
            {
                DoSomething((int)id);
            }
        }

        private static void DoSomething(int id)
        {
            Locks[id] = true;
            if (UseBarrier) Thread.MemoryBarrier();

            // ここのwhile条件を抜ける瞬間はLocks[自ID] == true
            // かつLocks[他ID] == false。
            // つまり、メモリ読み書きが機械語の順に実行されているなら
            // 他スレッドは同時にはこの判定を行えないので、以下しばらくの
            // 範囲は同時に実行されることがない。
            while (Locks[id ^ 1])
            {
                Locks[id] = false;
                if (UseBarrier) Thread.MemoryBarrier();
                Locks[id] = true;
                if (UseBarrier) Thread.MemoryBarrier();
            }
            // 同時に1スレッドしか実行されないはずの範囲 -- ここから

            int tmp = Value;
            if (tmp != -1)
            {
                // [何かヘン!]
                // ここに入る条件を満たすには、他スレッドが(★)の部分にいなければならないが、
                // メモリ操作の追い越しがなければそんなことにはならないはず。
                Console.WriteLine("Thread #{0}: Value = {1}, myLock = {2}, anotherLock = {3}",
                    id, tmp, Locks[id], Locks[id ^ 1]);
            }
            Value = id;
            if (UseBarrier) Thread.MemoryBarrier();
            // (★) Valueが-1以外の値を取るのはこの部分だけ。
            Value = -1;
            if (UseBarrier) Thread.MemoryBarrier();
            // 同時に1スレッドしか実行されないはずの範囲 -- ここまで
            Locks[id] = false;
            if (UseBarrier) Thread.MemoryBarrier();
        }
    }

<余談>
上記コード、以前GCC向けのCで書いたコードを、余計な部分を削りつつC#に移植したものです。
元コードはこちら。
http://slashdot.jp/journal/544783
</余談>

引用返信 編集キー/
■65902 / inTopicNo.11)  Re[9]: 変数のアトミック性を保障するコーディング方法について
□投稿者/ howling (259回)-(2013/03/22(Fri) 10:00:50)
No65894 (yoh2 さん) に返信
うおぉぉ…C#に変換までして頂いてありがとうございます!!
C++でも全然大丈夫だったのですが、個人的にはC#の方がコンパイラがうるさくて好きです(笑
むしろCはダメかな…。「C/C++ができます」という書き方はよくされますが、Cで全部本当に書ける人は少ないかなと。

とりあえず頂いたソースコード動かしてみます。勉強になります!ありがとうございます!!
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -