C#::全力でBeginInvokeするとどうなる?
答)死にます。その技は【秘技・三年殺し】と呼ばれています
なんのこっちゃわからんと思うので、サンプルを書いてみた。フォームがあってボタンが二個とラベルが張ってあると思いねぇ。
//using System.Threading; volatile bool running; private void button1_Click(object sender, EventArgs e){ var th = new Thread(() => { running = true; int i = 0; while(running) { i++; BeginInvoke((Action<int>)update,i); Thread.Sleep(1); } }); th.Start(); } private void button2_Click(object sender, EventArgs e){ running = false; } private void update(int i){ label1.Text = i.ToString(); }
読めばわかると思いますが、ボタンを押すとラベルに数字を0から高速でカウントアップしていきます。
では仮に、updateの処理に時間がかかるとしましょうか。たとえば、中でGDIを使って超複雑な図形を描画したりすると時間がかかります。簡単に再現させるとしたらこんな感じですかね?(いやまぁマシンの性能によっては上の状態ですでに間に合わない可能性もありますけれども)
private void update(int i){ Thread.Sleep(2); label1.Text = i.ToString(); }
これが何を意味するかというと、1msに一回呼び出されるBeginInvokeによる再描画要求に対して、その処理を一回するのに2msかかるということですね。ぜんぜん間に合いません。これを実行するとどうなるかというと、死にます。画面は応答なしになり、メモリ使用量はうなぎのぼり。で、そのうち死にます。どういう死に方をするかは恐ろしくて試していませんが、霊力を吸収しすぎた白虎様のように弾けて死んでしまうことが予想されます。破裂して死んでしまう前にタスクマネージャで殺してあげるのが優しさというものでしょう。
どうしてこんなことになるかといえば、BeginInvokeというものは、キューに関数呼び出しをためておいて、そのトリガーメッセージをPostMessageで投げているだけなので、処理が終わらないとキューがどんどん肥大化するのですね。ちなみに、普通のInvokeであれば描画が終わるのを待つのでこの問題は発生しませんが、かわにスレッドでやりたいこと(このサンプルではiのインクリメント)の処理速度が描画速度で制限されます。
ではこれをどうやって解決するかというと、まぁ描画が追いついてないだけなのでフレームスキップすればよいのですよ。単純には、描画が終わる前に発生する再描画要求は無視する、と。
volatile bool running; volatile bool beforeUpdate; private void button1_Click(object sender, EventArgs e){ var th = new Thread(() => { running = true; int i = 0; while(running){ i++; if (!beforeUpdate){ beforeUpdate = true; Invoke((Action<int>)update, i); } Thread.Sleep(1); } //最後付近のupdateがスキップされているかもしれないので //最後に一度だけ更新しておく Invoke((Action<int>)update, i); }); th.Start(); } private void button2_Click(object sender, EventArgs e){ running = false; } private void update(int i){ if (beforeUpdate){ Thread.Sleep(2); label1.Text = i.ToString(); beforeUpdate = false; } }
万全を期すならば、さらにbeforeUpdateの操作周辺をlockしたりすべきでしょうね。Interlocked.Exchangeなんかでもいいかも。
やれやれ。マルチスレッド周辺は罠が多いですね?こういう「即死しない系」はなかなか見つからないバグの代表格なのです。