スレッド間のエラーを解消するには


竹尾  2013-03-28 20:21:53  No: 148130  IP: [192.*.*.*]

「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール・・・」とエラーが出て対処法がわからずに困って降ります。

流れとしては、
クラスを作成してそこに別スレッドで1秒毎にイベントを発生させています。
この発生したイベントをメインスレッドのイベントとして使用したいのです。
どうすれば良いのか解からず困っています。
どうか、御教授願えませんでしょうか。

Imports System
Imports System.Threading

Public Class Class1
    Public Event GetMow(ByVal sender As Object, ByVal e As EventArgs)

    Public Sub Run()
        Dim timerDelegate As TimerCallback _
          = New TimerCallback(AddressOf MyClock)
        Dim timer As Timer _
          = New Timer(timerDelegate, Nothing, 0, 1000)

        Console.ReadLine() ' キーが押されるまで待機
    End Sub

    Public Sub MyClock(o As Object)
        Console.WriteLine(DateTime.Now)
        RaiseEvent GetMow(Me, New EventArgs)
    End Sub

End Class


Public Class form1
    Dim WithEvents ss As New Class1
    Private Sub ss_GetMow(ByVal sender As Object, ByVal e As System.EventArgs) Handles ss.GetMow
        ProgramTimer.Label1.Text = Now.Minute
    End Sub
    Private Sub form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        ss.Run()
    End Sub

End Class

よろしくお願いいたします。

編集 削除
魔界の仮面弁士  2013-03-28 21:38:20  No: 148131  IP: [192.*.*.*]

> エラーが出て対処法がわからずに困って降ります。
コントロール(Form、TextBox、Label など)を、別スレッドから
操作することはできません。UI スレッドで生成されたコントロールは、
UI スレッドからのみ読み書きが可能です。


> 別スレッドで1秒毎にイベントを発生させています。
System.Windows.Forms.Timer では駄目なのでしょうか。


> この発生したイベントをメインスレッドのイベントとして使用したいのです。
慣れないうちは、BackgroundWorker を使うと分かりやすいかも。
ワーカースレッドから ReportProgress メソッドでイベントを発生させ、
画面側は ProgressChanged イベントでそれを受け取るようにします。


> どうすれば良いのか解からず困っています。
http://www.atmarkit.co.jp/fdotnet/dotnettips/312ctrlinvoke/ctrlinvoke.html
http://www.atmarkit.co.jp/fdotnet/dotnettips/373threadtimer/threadtimer.html


> どうか、御教授願えませんでしょうか。
http://www.tt.rim.or.jp/~rudyard/torii009.html
http://blogs.wankuma.com/jeanne/archive/2005/11/24/19566.aspx

編集 削除
竹尾  2013-03-29 17:21:37  No: 148132  IP: [192.*.*.*]

ご指摘有難うございます。

BackgroundWorkerを使用したのですが、Label1には何も表示されません。
BackgroundWorker1_ProgressChangedは別スレッド扱いなのでしょうか。

Imports System
Imports System.Threading
Public Class Form1
    Dim ss As New Class1
    Private Sub form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        ss.Run()
        BackgroundWorker1.RunWorkerAsync()
    End Sub
    Private Sub BackgroundWorker1_ProgressChanged _
        (sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) _
        Handles BackgroundWorker1.ProgressChanged

        Debug.Print(e.ProgressPercentage)
        Label1.Text = e.ProgressPercentage
    End Sub
End Class

Public Class Class1
    Public Sub Run()
        Dim timerDelegate As TimerCallback _
          = New TimerCallback(AddressOf MyClock)
        Dim timer As Timer _
          = New Timer(timerDelegate, Nothing, 0, 1000)
    End Sub

    Public Sub MyClock(o As Object)
        Form1.BackgroundWorker1.WorkerReportsProgress = True
        Form1.BackgroundWorker1.ReportProgress(CInt(DateTime.Now.Second))
    End Sub
End Class

編集 削除
魔界の仮面弁士  2013-04-02 11:52:14  No: 148133  IP: [192.*.*.*]

> BackgroundWorkerを使用したのですが、Label1には何も表示されません。
使い方が間違っています。

BackgroundWorker を使うなら、RunWorkerAsync メソッドから
実行されるための「DoWork イベント」の記述が必要です。

今回のコードでは、Form1 に DoWork イベントの処理が記載されて
いないため、ワーカースレッドは「何もせずに即座に終了」しています。

この場合、「BackgroundWorker のワーカースレッド」と
「UI スレッド」と「TimerCallback のスレッド」が
それぞれ別物であることに注意してください。


スレッド管理を BackgroundWorker に任せたのであれば、
本来は TimerCallback の出番はありません。
今回のサンプルであれば、DoWork イベントの中で
「1000ミリ秒 Sleep させてから、ReportProgress を呼ぶ」ことで、
処理できるでしょう。それがやりたいことに合致しているかは別として。


> BackgroundWorker1_ProgressChangedは別スレッド扱いなのでしょうか。

BackgroundWorker クラスの ProgressChanged イベントは、
  「RunWorkerAsync メソッドを呼び出したスレッド」
にて呼び出されます。今回の場合は Form1 の UI スレッドですね。


> Public Sub MyClock(o As Object)
>     Form1.BackgroundWorker1.WorkerReportsProgress = True
>     Form1.BackgroundWorker1.ReportProgress(CInt(DateTime.Now.Second))
> End Sub
上記のようなことは、絶対に行ってはいけません。

マルチスレッドプログラミングにおいては、操作できるのは基本的に、
自身のスレッド上で動いているオブジェクトのみに限定されます。

不用意に他のスレッドのオブジェクトの状態を変化させてしまうと、
同時実行制御の上で問題を生じ、再現させにくいトラブルを
生じさせてしまう要因となります。


どうしても他のスレッドに進捗情報を伝える必要がある場合には、
以下のようにして対処することができます。

(案1) 相手側のスレッドに対して情報を通知し、実際の処理は
  相手のスレッドに処理を「依頼」する形をとる。
  → BackgroundWorker はこの方法をとるために、内部で
    AsyncOperationManager / AsyncOperation クラスを利用しています。
    BackgroundWorker を用いず、自身で管理しているスレッドから
    UI スレッドを操作したいような場合には、前回紹介した URL に
    記載されている方法を使うのが良いでしょう。

(案2) フィールド変数やプロパティのアクセスなどは避け、
  常に「スレッドセーフな呼び出しが保証されたメソッド」のみを使う。
  → スレッドセーフなコードであれば、複数のスレッドから
    同時並行的に実行されても、特に問題は生じません。

(案3) いずれかのスレッドが状態を変更させている間は、
    他のスレッドがその変数やプロパティを読み書きしないように
    何かしらの同時実行制御や排他制御を施す。
  → ReaderWriterLock や Interlocked などのクラスを通じて
    変数を操作するようにするとか、あるいは SyncLock で囲むとか。


# 具体的な手順は、前回紹介した URL をご覧ください。

編集 削除