_DoWork内でCombobox値取得について

タグの編集
投稿者 HAL  (社会人) 投稿日時 2009/2/21 01:06:11
いつも勉強させて頂いております。

環境は
Windows XP Professional SP3
Visual Studio 2008 です。

お恥ずかしい質問なのですが、FormにComboboxとButtonを配置し、
Buttonを押した際に、BackgroundWorkerにて処理をします。

その際に、_DoWork 内でCombobox.SelectedItemや.SelectedIndexの値を
取得したいのですが非同期なので当たり前でしょうが、怒られてしまいます。

どの様にすれば、
DoWork 内でCombobox.SelectedItemや.SelectedIndexの値を取得できるのでしょうか?
現在は、Butttonを押した際にこれらの値を共通変数に格納して利用している状況です。

この様な形でなくスムースな値取得のご指導を何卒宜しくお願い致します。
投稿者 太郎冠者  (社会人) 投稿日時 2009/2/21 01:23:52
この辺とか
[Windowsフォームで別スレッドからコントロールを操作するには?]
http://www.atmarkit.co.jp/fdotnet/dotnettips/312ctrlinvoke/ctrlinvoke.html
投稿者 HAL  (社会人) 投稿日時 2009/2/21 01:34:43
太郎冠者様、お早いご指導誠に有難う御座います。

Delegate/Invoke なんですよね。
この様な恥ずかしい質問をしている位のレベルですから、
http://www.atmarkit.co.jp/fdotnet/dotnettips/312ctrlinvoke/ctrlinvoke.html 
も書き込む前に拝見しておりましたが、Delegate/Invokeが理解できなく且つ
@ITサイトの説明は私の様なレベルでは難しく理解できませんでした。

Delegate/Invokeを知識に加えたいのですが、より分かりやすいサイトはご存じではないでしょうか?

また、お手数でなければ、ご指導お願い致します。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/21 02:16:34
処理に必要なデータ(ComboBox1.SelectedItem など)があれば、
RunWorkerAsync の呼び出し時に、引数として渡しておけば OK です。

DoWork イベント内では、そのデータを e.Argument で取得できます。


> 共通変数に格納して利用している状況です。

その共通変数は、スレッドセーフになってますか?

たとえば、As Integer な変数でさえ、そのままではスレッドセーフではないため、
Interlocked クラスや ReaderWriterLock クラスを使う必要があります。
(SyncLock ステートメントという手もありますが、これの使用は最小限にすべき)
投稿者 HAL  (社会人) 投稿日時 2009/2/21 02:39:30
魔界の仮面弁士様、ご指導大変有難う御座います。

RunWorkerAsyncの引数でいけるんですね。
その引数の数制限はないのでしょうか?

共通変数と勝手に述べたのは、Class Form 直後に
Private strComboItem as string = String.Empty みたく記述して
Button_Click内にて、
strComboItem = Combo.SelectedItem
RunWorkerAsync()
の様な感じです。
これで、_DoWork内でstrComboItemの値取得出来たのですが、
また低レベルな方法でしたでしょう。お恥ずかしいです。
投稿者 太郎冠者  (社会人) 投稿日時 2009/2/21 03:09:10
>RunWorkerAsyncの引数でいけるんですね。
>その引数の数制限はないのでしょうか?

RunWorkerAsyncに引数として渡せるのは、1個のObject型だけです。
なので、複数の値を渡したければ、それらを格納するクラスか構造体を定義するか
もっと単純に配列に突っ込んで渡すしかありません。

例)
'配列に放り込んで渡す 
BackgroundWorker1.RunWorkerAsync(New Object() {"a""b""c", 1, 2, 3})

投稿者 HAL  (社会人) 投稿日時 2009/2/21 03:19:56
太郎冠者様、またご指導誠に有難う御座います。

BackgroundWorker1.RunWorkerAsync(New Object() {"a", "b", "c", 1, 2, 3})
大変分かりやすく納得致しました。

2週間程前まで古いと言われてしまいますが、VB6を使用していました。
ですので全然慣れません。

しかしBackgroundWorker _DoWorkは疑問不思議一杯です。
if check1.checked then は、_DoWork内で答えてくれるのに
combo,label,text等は答えてくれないのでしょうね…
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/21 05:47:06
BackgroundWorker を使う際の基本は――

開始パラメータは、
  メインスレッド:RunWorkerAsyncの引数で渡す
  ワーカースレッド:DoWork 内で e.Argument で受け取る
途中経過報告は、
  ワーカースレッド:DoWork 内で ReportProgress で通知する
  メインスレッド:ProgressChanged で受信し、画面等に表示
途中キャンセル要求は、
  メインスレッド:CancelAsync メソッドで中止依頼を行う
  ワーカースレッド:DoWork 内で CancellationPending の状態を見て処理を打ち切る
処理結果は、
  ワーカースレッド:DoWork 内で e.Result で渡す
  メインスレッド:RunWorkerCompleted 内で e.Result で受け取る

――という流れになります。
DoWork 内では、フォーム上のコントロールやフィールド変数を読み書きしないようにしてください。

「BackgroundWorker に処理を依頼したら、後は終わるまで放置」が原則です。

許されるのは、ReportProgress や CancelAsync によるやりとりだけであり、DoWork 内では、
「イベント引数」と「イベント内のローカル変数」以外の変数は、原則として使用禁止と思ってください。

当然、「Private strComboItem as string」も使えません。
DoWork 中は絶対に書き換えないという事が保証できるなら、読取専用として利用できなくも無いですが、
それならば RunWorkerAsync の引数で十分であるため、実質的には使えない事になります。


どうしても、非同期処理中にメインスレッドとのやり取りが必要となる場合には、
変数の変更と読込が同時に発生してしまわないようにするために、Invoke メソッドで処理を依頼するか、
あるいは、何かしらの同期/ロック機構を用意する必要があるでしょう。


> if check1.checked then は、_DoWork内で答えてくれるのに
本来はそれも NG です。
コントロールは、メインスレッド(UIスレッド)からのみ操作が許可されています。
投稿者 HAL  (社会人) 投稿日時 2009/2/21 09:41:54
魔界の仮面弁士様、再度、ご指導大変有難う御座います。

さて、そうなると実は他のサイトでも類似した質問をさせて頂いたのですが、
例としてform_mainに、text1(ここにaaa.wavのフルパス)、
combo1(MP3変換の為のビットレート)、combo2(同、チャンネル)、
combo3(同、サンプリングレート)、button1(変換)、button2(キャンセル)を配置し、
button1(変換)クリック時に保存先ディレクトリダイアログ表示後、
lame.exeを実行しエンコードと言う処理がしたいとします。

その際、仮に直でこのfrom_mainのbutton1_clickイベント時にlame.exeをshellなり、
process.startで実行させた際、form_mainは操作不能となり、
button2(キャンセル)はクリックできずに変換処理の強制終了は出来ませんでした。

となると、非同期でlame.exeを実行させることを考えました。
そうするとcombo1-3のデータは必要となります。

>DoWork 内では、「イベント引数」と「イベント内のローカル変数」以外の変数は、
 原則として使用禁止と思ってください。
とおっしゃいましたが、
ソース上システムに影響のないもので原則として使用禁止というのが分かりません。
何をもって原則として使用禁止なんでしょうか?

また、ご指導でおっしゃっている事を考えるとこの上記の例の場合で、
_DoWorkでなく且つcombo1-3の値を引き継ぐ方法があるのだと受け取れました。

因みに上記の例はキャンセルクリック不可で動作確認済みで、結果aaa.wav→aaa.mp3に変換できましたし、_DoWork内でもキャンセルクリック可能/lame.exe プロセス強制終了OKで同結果で変換できました。
となると、やはり原則として使用禁止が気になります…。

何卒、VB2008若輩者にご指導お願い致します。
投稿者 HAL  (社会人) 投稿日時 2009/2/21 09:46:43
追記:当方、プログラマーやSE関連の職ではなく音楽関連の職の為、本当のVB小学生(?)です。
投稿者 太郎冠者  (社会人) 投稿日時 2009/2/21 10:35:58
>さて、そうなると実は他のサイトでも類似した質問をさせて頂いたのですが
これですかね。
こちらのVB中学校様はそういった規約は無いみたいですけど、VBレスキュー様は原則マルチポストは禁止
だったと思います。
http://hanatyan.sakura.ne.jp/vbnetbbs/wforum.cgi?mode=allread&no=8742&page=0

このまま同じ話題を続けるおつもりであるなら、どちらか一方の掲示板にて展開するようにし
両方の掲示板にその旨通達すべきです。

投稿者 HAL  (社会人) 投稿日時 2009/2/21 11:21:01
太郎冠者様、ご指摘有難う御座いますと共にお詫び申し上げます。

しかしながら、他サイト後にVB中学校様にてご質問させて頂き
且つ質問意図が全く異なる内容で、あくまでも例として簡単な仕様を記述したもので御座います。

それもマルチポストになるのでしょうか?
であれば、他サイトは既に完了している為、ご指導の方宜しくお願い致します。
投稿者 かずき  (社会人) 投稿日時 2009/2/22 19:35:57
>> DoWork 内では、「イベント引数」と「イベント内のローカル変数」以外の変数は、
>> 原則として使用禁止と思ってください。
> とおっしゃいましたが、
> ソース上システムに影響のないもので原則として使用禁止というのが分かりません。
> 何をもって原則として使用禁止なんでしょうか?
コントロールについては、ドキュメントに別スレッドから使ったらだめと書いてあるからです。
http://msdn.microsoft.com/ja-jp/library/system.windows.forms.control.aspx

MSDNから引用:
コントロールのハンドルが既に作成されている場合、スレッド セーフであるメンバは、
BeginInvoke、EndInvoke、Invoke、InvokeRequired、および CreateGraphics だけです。
コントロールのハンドルがバックグラウンド スレッドで作成される前に 
CreateGraphics を呼び出すと、無効なスレッド間の呼び出しが発生する可能性があります。 


マルチスレッドのプログラミングについて調べられてみるのがいいと思います。
BackgroundWorkerは、簡単にマルチスレッドのプログラムが組めるようになる
機能を提供するクラスなので、やったら駄目なことや、気をつけることはマルチスレッドで
プログラミングするときと同じです。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2009/2/22 22:28:44
> 何をもって原則として使用禁止なんでしょうか?
すでにかずきさんが答えておられますが、System.Windows.Forms.Control の派生クラス(いわゆる「コントロール」)のメンバは、UIスレッド以外からの操作に対応していないのです。
BackgroundWorker のように、System.ComponentModel.Component の派生クラス(いわゆる「コンポーネント」)であれば、別スレッドから呼び出しても構いませんが、メッセージベースの処理を行うようなコンポーネント(たとえば、System.Windows.Forms.Timer など)は、UIスレッドからしか使えません。


> また、ご指導でおっしゃっている事を考えるとこの上記の例の場合で、
> _DoWorkでなく且つcombo1-3の値を引き継ぐ方法があるのだと受け取れました。
そうですね。ビットレート等を、変換中に変更させる必要が無いのであれば、
e.Argument で開始パラメータとして受け取っておけば対応できるかと思います。


> 同結果で変換できました。
マルチスレッド処理では、同時実行制御が重要になります。

たとえば、
 Private money As Integer = 1000   '所持金1000円
というフィールド変数があったとします。

そしてそれを、処理AとBが、それぞれ 1回ずつ
 money += 1   '所持金に 1 円加える。
というコードを実行することを想像してみてください。
AとBとで計 2 回加算されますから、処理結果は 1002 円になりますよね。


ところが、これをマルチスレッドで処理する場合は、そうなるとは限りません。
この処理を、(ほぼ)同時に実行すると、微妙なタイミングの差で異なる金額になってしまう事があります。

《パターン1》
(1) Aが、変数money から値を読み取る。(Aのメモリ値=1000、money=1000)
(2) Aが、読みとった値に +1 を加える。(Aのメモリ値=1001、money=1000)
(3) Bが、変数money から値を読み取る。(Bのメモリ値=1000、money=1000)
(4) Bが、読みとった値に +1 を加える。(Bのメモリ値=1001、money=1000)
(5) Aが、加算した値を変数に書き戻す。(Aのメモリ値=1001、money=1001)
(6) Bが、加算した値を変数に書き戻す。(Bのメモリ値=1001、money=1001)
(7) 変数 money の最終値は 1001 円。

《パターン2》
(1) Aが、変数money から値を読み取る。(Aのメモリ値=1000、money=1000)
(2) Aが、読みとった値に +1 を加える。(Aのメモリ値=1001、money=1000)
(3) Aが、加算した値を変数に書き戻す。(Aのメモリ値=1001、money=1001)
(4) Bが、変数money から値を読み取る。(Bのメモリ値=1001、money=1001)
(5) Bが、読みとった値に +1 を加える。(Bのメモリ値=1002、money=1001)
(6) Bが、加算した値を変数に書き戻す。(Bのメモリ値=1002、money=1002)
(7) 変数 money の最終値は 1002 円。


単純な Integer 型の変数でさえこの始末です。
しかもこれは、とてもシビアなタイミングで発生する現象であり、『ほとんどの場合は上手くいくが、ごく稀に失敗する』という事態になりえます。

時には、1000万回実行してようやく再現するかも知れませんし、あるいは最初の数回で再現するかも知れません。
このようなバグは、一度作り込んでしまうと、後から発見・修正する事が非常に難しいのです。そのため、最初から正しい設計をしなくてはなりません。
(.NET 2.0 以降では、異なるスレッドからの操作ができないようチェックする仕組みが登載されていますが、完璧に検出できるわけではありません)

そしてスレッドが安全に実行されるようにするには、たとえば
 (案1)ローカル変数以外は使わない。(変数を異なるスレッドで共有しない)
 (案2)非ローカル変数を利用するが、1つの変数を1つのスレッドだけで利用する。
 (案3)1つの変数を複数の非同期スレッドから利用するが、変数のアクセス部分だけは同期化させ、共有データが同時にアクセスさせれないよう排他制御する。
といった対策が必要とされるというわけです。


そしてもっとも簡単なのは 案1 であり、私が推奨していた方法がこれにあたります。
投稿者 HAL  (社会人) 投稿日時 2009/2/23 08:36:35
魔界の仮面弁士様、とても分かり易いご指導誠に有難う御座います。
素人がPGを組む事は宜しくないんでしょうね(笑)
どうしても他の方が作っていない自分には便利なアプリを作ろうと思い久々に
VB6→VB2008にバージョンアップして作ろうと思っていた次第だったのです。

多分、魔界の仮面弁士様のご指導から行くと、
Sysytem.Threading.Thread(逆だったらすみません)も
同様に私の当初の考えではダメなんでしょうね…

最後に話題がズレますがPHPをHPにてかじった事がある方は、
C#の方がVBよりいいのでしょうか?