• 追加された行はこの色です。
  • 削除された行はこの色です。
#title(.NETプログラミング研究 第25号)

#navi(.NET プログラミング研究)
#navi(.NETプログラミング研究)

#contents

*.NETプログラミング研究 第25号 [#a503a075]

**.NET Tips [#pc3e9439]

**.NETのマルチスレッドプログラミング その7 [#vdc5818c]

#column(注意){{
この記事の最新版は「[[.NETのマルチスレッドプログラミング>http://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。
この記事の最新版は「[[.NETのマルチスレッドプログラミング>https://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。
}}

***別スレッドからフォーム、コントロールを扱う [#yf546e81]

マルチスレッドプログラミングで注意すべき様々な問題について、第21号の「スレッドの同期」などで説明してきました。GUIのWindowsアプリケーションでは、これらに加えて、さらに気をつけるべきことがあります。

というのは、コントロール(フォームも含む)のメソッドは、そのコントロールを作成したスレッドからのみ呼び出される必要があるからです(注1)。(これは、Windowsフォームがシングルスレッドアパートメント(STA)モデルを使用しているためです。詳しくはヘルプの「マルチスレッド Windows フォーム コントロールのサンプル」等をご覧ください。)コントロールを作成したスレッド以外のスレッドからそのコントロールのメソッドを呼び出すには、マーシャリングにより、コントロールを作成したスレッドからメソッドを呼び出します。このマーシャリングには多くのリソースを消費しますが、これを確実且つ効率よく行う方法として、ControlクラスにはInvoke、BeginInvoke、およびEndInvokeメソッドが用意されています。

-[[マルチスレッド Windows フォーム コントロールのサンプル>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcondevelopingmultithreadedwindowsformscontrol.asp]]

(注1:ただし、Invoke、BeginInvoke、EndInvoke、CreateGraphicsメソッドの呼び出しはスレッドセーフであり、その必要がありません。)

まずはInvokeメソッドの使用法を説明します。Invokeメソッドを使う際の手順は、

+コントロールのメソッドを呼び出すためのメソッドを作成する。
+そのメソッドに合ったデリゲートを宣言する。
+コントロール(あるいはコントロールの親フォーム)のInvokeメソッドをデリゲートオブジェクトを指定して呼び出す。

といったようになります。

次にInvokeメソッドを使った具体例を示します。ここでは、Button1をクリックすることにより新しいスレッドを作成し、このスレッドによりTextBox1に0から99までの数を順番に表示させています。なお、このコードはFormクラス内に書かれているものとします。

#code(vbnet){{
'Imports System.Threading
'が宣言されているものとする

'Invokeメソッドで使用するデリゲート
Delegate Function AddStringDelegate(ByVal str As String) As Integer

Private Sub Button1_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles Button1.Click
    'TextBox1の初期化
    TextBox1.Text = ""
    'スレッドの作成
    Dim t As New Thread(New ThreadStart(AddressOf DoSomething))
    t.IsBackground = True
    'スレッドを開始する
    t.Start()
End Sub

'別スレッドで実行するメソッド
Private Sub DoSomething()
    'AddStringDelegateの作成
    Dim dlg As New AddStringDelegate(AddressOf AddString)

    Dim i As Integer
    For i = 0 To 99
        'TextBox1に追加する文字列
        Dim str As String = i.ToString()
        'TextBox1に文字列を追加する
        'TextBox1は別スレッドが所有しているため、
        'Invokeを使ってTextBox1を操作する
        Dim count As Integer = _
            CInt(TextBox1.Invoke(dlg, New Object() {str}))
        '一秒停止
        Thread.Sleep(1000)
    Next i
End Sub

'TextBox1に文字列を追加する
Private Function AddString(ByVal str As String) As Integer
    TextBox1.AppendText((str + ControlChars.CrLf))
    Return TextBox1.Lines.Length
End Function
}}

#code(csharp){{
//using System.Threading;
//が宣言されているものとする

//Invokeメソッドで使用するデリゲート
private delegate int AddStringDelegate(string str);

private void Button1_Click(object sender, System.EventArgs e)
{
    //TextBox1の初期化
    TextBox1.Text = "";
    //スレッドの作成
    Thread t = new Thread(new ThreadStart(DoSomething));
    t.IsBackground = true;
    //スレッドを開始する
    t.Start();
}

//別スレッドで実行するメソッド
private void DoSomething()
{
    //AddStringDelegateの作成
    AddStringDelegate dlg = new AddStringDelegate(AddString);

    for (int i = 0; i < 100; i++)
    {
        //TextBox1に追加する文字列
        string str = i.ToString();
        //TextBox1に文字列を追加する
        //TextBox1は別スレッドが所有しているため、
        //Invokeを使ってTextBox1を操作する
        int count = (int) TextBox1.Invoke(dlg, new object[] {str});
        //一秒停止
        Thread.Sleep(1000);
    }
}

//TextBox1に文字列を追加する
private int AddString(string str)
{
    TextBox1.AppendText(str + "\r\n");
    return TextBox1.Lines.Length;
}
}}

上記の例では、TextBox1を作成したスレッドはメインスレッドですので、それ以外のスレッド"t"からTextBox1の内容を変更するために、Invokeメソッドを使用しています。Invokeメソッドを通じてAddStringメソッドを呼び出すことにより、AddStringメソッドはTextBox1を作成したメインメソッドで実行されるようになります。

次にBeginInvoke、EndInvokeメソッドを使ってみましょう。Invokeメソッドが同期的に実行するのに対して、BeginInvokeメソッドは非同期的に実行します。EndInvokeメソッドはBeginInvokeメソッドにより実行されたメソッドの返り値を取得するために使用します。BeginInvokeメソッドを呼び出したときに非同期操作が終了していなければ、終了するまでブロックされます。

先ほどの例をBeginInvoke、EndInvokeメソッドを使って書き換えてみましょう。DoSomethingメソッド以外に変更はありませんので、DoSomethingメソッドのみ示します。

#code(vbnet){{
'別スレッドで実行するメソッド
Private Sub DoSomething()
    'AddStringDelegateの作成
    Dim dlg As New AddStringDelegate(AddressOf AddString)

    Dim i As Integer
    For i = 0 To 99
        'TextBox1に追加する文字列
        Dim str As String = i.ToString()
        'TextBox1に文字列を追加する
        'TextBox1は別スレッドが所有しているため、
        'Invokeを使ってTextBox1を操作する
        Dim ar As IAsyncResult = _
            TextBox1.BeginInvoke(dlg, New Object() {str})
        '返り値を取得する
        Dim count As Integer = CInt(TextBox1.EndInvoke(ar))
        '一秒停止
        Thread.Sleep(1000)
    Next i
End Sub
}}

#code(csharp){{
//別スレッドで実行するメソッド
private void DoSomething()
{
    //AddStringDelegateの作成
    AddStringDelegate dlg = new AddStringDelegate(AddString);

    for (int i = 0; i < 100; i++)
    {
        //TextBox1に追加する文字列
        string str = i.ToString();
        //TextBox1に文字列を追加する
        //TextBox1は別スレッドが所有しているため、
        //Invokeを使ってTextBox1を操作する
        IAsyncResult ar = TextBox1.BeginInvoke(dlg, new object[] {str});
        //返り値を取得する
        int count = (int) TextBox1.EndInvoke(ar);
        //一秒停止
        Thread.Sleep(1000);
    }
}
}}

変わったのはInvokeの代わりにBeginInvokeとEndInvokeを使用している所だけです(BeginInvokeの後すぐにEndInvokeを呼び出しているため、非同期の意味がありませんが)。BeginInvokeを呼び出したときにIAsyncResultオブジェクトを取得し、EndInvokeで返り値を取得するために使用します。返り値を取得する必要が無ければ、EndInvokeを呼び出す必要はありません。

あるコントロールに対してInvokeメソッドを使ってメソッドを呼び出す必要があるか調べる方法として、Control.InvokeRequiredプロパティが用意されています。そのコントロールを作成したスレッドがInvokeRequiredを呼び出したスレッドと異なる場合はTrue、それ以外はFalseを返します。

InvokeRequiredプロパティを調べ、Invokeメソッドを使う必要があればInvokeを使い、そうでなければ使わないでメソッドを呼び出すコード例を以下に示します。ここでもDoSomethingメソッドのみ示し、それ以外は前と同じです。

#code(vbnet){{
'別スレッドで実行するメソッド
Private Sub DoSomething()
    'AddStringDelegateの作成
    Dim dlg As New AddStringDelegate(AddressOf AddString)
    Dim count As Integer

    Dim i As Integer
    For i = 0 To 99
        'TextBox1に追加する文字列
        Dim str As String = i.ToString()
        'TextBox1に文字列を追加する
        'InvokeRequiredによりInvokeが必要か調べる
        If TextBox1.InvokeRequired Then
            'Invokeが必要な時はInvokeによりデリゲートを実行
            count = CInt(TextBox1.Invoke(dlg, New Object() {str}))
        Else
            'Invokeが必要ないときはそのままデリゲートを実行
            count = CInt(dlg.DynamicInvoke(New Object() {str}))
        End If
        '一秒停止
        Thread.Sleep(1000)
    Next i
End Sub
}}

#code(csharp){{
//別スレッドで実行するメソッド
private void DoSomething()
{
    //AddStringDelegateの作成
    AddStringDelegate dlg = new AddStringDelegate(AddString);
    int count;

    for (int i = 0; i < 100; i++)
    {
        //TextBox1に追加する文字列
        string str = i.ToString();
        //TextBox1に文字列を追加する
        //InvokeRequiredによりInvokeが必要か調べる
        if (TextBox1.InvokeRequired)
            //Invokeが必要な時はInvokeによりデリゲートを実行
            count = (int) TextBox1.Invoke(dlg, new object[] {str});
        else
            //Invokeが必要ないときはそのままデリゲートを実行
            count = (int) dlg.DynamicInvoke(new object[] {str});
        //一秒停止
        Thread.Sleep(1000);
    }
}
}}

このようなコードを書くことにより、DoSomethingメソッドはどのスレッドから呼び出しても適切にInvokeの使用を切り替えることができるようになります。

補足: ここでは
TextBox1.Invoke
としましたが、 TextBox1のあるフォームのInvokeメソッドを呼び出しても結構です(むしろその方が良いケースも多いでしょう)。この場合、上記のコードでは、
this.Invoke (VB.NETでは、Me.Invoke)
となります。 

参考:

-[[コンポーネントのマルチスレッド>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbconScalabilityMultithreadingInComponents.asp]]
-[[スレッドからのコントロールの操作>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbtskmanipulatingcontrolsfromthreads.asp]]
-[[チュートリアル : Visual Basic による簡単なマルチスレッド コンポーネントの作成>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbconwalkthroughsimplemultithreadedcomponent.asp]]
-[[チュートリアル : Visual C# による簡単なマルチスレッド コンポーネントの作成>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbwlkwalkthroughcreatingsimplemultithreadedcomponenetwithvisualc.asp]]
-[[スレッドからのコントロールの操作>http://www.microsoft.com/japan/msdn/library/ja/vbcon/html/vbtskmanipulatingcontrolsfromthreads.asp]]
-[[マルチスレッド Windows フォーム コントロールのサンプル>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcondevelopingmultithreadedwindowsformscontrol.asp]]
-[[フォームとコントロールでのマルチスレッド>http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconFreeThreadingWithFormsControls.asp]]

***スレッドへデータを渡す、スレッドからデータを取得する [#j3a5fde6]

スレッドプールを使ってスレッドを開始する方法では、スレッドとデータのやり取りをする方法が用意されていますが、Thread.Startメソッドでスレッドを開始した場合、そのスレッドへデータを渡したり、スレッドからデータを取得することは簡単ではありません。なぜなら、Thread.Startメソッドに必要なThreadStartデリゲートには引数も戻り値もありませんので、引数や戻り値を持つメソッドによりスレッドを開始することは出来ないからです。そこで、別の方法を考える必要があります。

まず考えられるのが、データを渡す側、取得する側両方のスレッドからアクセスできる変数を使ってデータのやり取りするという方法です。

ただしこの方法でマルチスレッドメソッドから戻り値を取得するには、戻り値を返すスレッドがその結果を変数に確実に代入したことをその戻り値を取得するスレッドの側で知る必要があります。下に示したサンプルコードでは、Joinメソッドを使って戻り値を返すスレッドが終わるまで待機してからその結果を取得しています。

このコードでは、MyThreadメソッドを別スレッドで開始し、sleepTimeフィールドを介してMyThreadメソッドにデータを渡し、returnValueフィールドを介してMyThreadメソッドの結果を渡すようにしています。

#code(vbnet){{
Class MainClass
    'エントリポイント
    Public Shared Sub Main()
        'スレッドに渡すデータを設定する
        sleepTime = 10000

        'Threadオブジェクトを作成する
        Dim t As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf MyThread))
        'スレッドを開始する
        t.Start()

        'スレッドtが終わるまで待機する
        t.Join()

        '結果を表示する
        Console.WriteLine(returnValue)

        Console.ReadLine()
    End Sub

    'スレッドが返す値
    Private Shared returnValue As Integer
    'スレッドに渡す値
    Private Shared sleepTime As Integer

    '別スレッドで実行するメソッド
    Private Shared Sub MyThread()
        Dim start As Integer = System.Environment.TickCount

        '長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始")
        'sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(sleepTime)
        '処理が終わったことを知らせる
        Console.WriteLine("終わりました")

        '実際にかかった時間を返す
        returnValue = System.Environment.TickCount - start
    End Sub
End Class
}}

#code(csharp){{
class MainClass
{
    //エントリポイント
    public static void Main()
    {
        //スレッドに渡すデータを設定する
        sleepTime = 10000;

        //Threadオブジェクトを作成する
        System.Threading.Thread t = 
            new System.Threading.Thread(
                new System.Threading.ThreadStart(MyThread));
        //スレッドを開始する
        t.Start();

        //スレッドtが終わるまで待機する
        t.Join();

        //結果を表示する
        Console.WriteLine(returnValue);

        Console.ReadLine();
    }

    //スレッドが返す値
    private static int returnValue;
    //スレッドに渡す値
    private static int sleepTime;

    //別スレッドで実行するメソッド
    private static void MyThread()
    {
        int start = System.Environment.TickCount;

        //長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始");
        //sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(sleepTime);
        //処理が終わったことを知らせる
        Console.WriteLine("終わりました");

        //実際にかかった時間を返す
        returnValue = System.Environment.TickCount - start;
    }
}
}}

ヘルプ「マルチスレッド プロシージャのパラメータと戻り値」では、別スレッドとして実行するメソッドはクラスにラップし、パラメータとして機能するフィールドをそのクラスに定義するのが「最善の方法」であると述べられています。これに従って先のコードを書き直すと次のようになります。

-[[マルチスレッド プロシージャのパラメータと戻り値>http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vaconparametersreturnvaluesforfreethreadedprocedures.asp]]

#code(vbnet){{
'マルチスレッドメソッドのためのクラス
Class SampleClass
    'スレッドが返す値
    Public ReturnValue As Integer
    'スレッドに渡す値
    Public SleepTime As Integer

    '別スレッドで実行するメソッド
    Public Sub MyThread()
        Dim start As Integer = System.Environment.TickCount

        '長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始")
        'sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(SleepTime)
        '処理が終わったことを知らせる
        Console.WriteLine("終わりました")

        '実際にかかった時間を返す
        ReturnValue = System.Environment.TickCount - start
    End Sub
End Class

Class MainClass
    'エントリポイント
    Public Shared Sub Main()
        'SampleClassオブジェクトの作成
        Dim sample As New SampleClass
        'スレッドに渡すデータを設定する
        sample.SleepTime = 10000

        'Threadオブジェクトを作成する
        Dim t As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf sample.MyThread))
        'スレッドを開始する
        t.Start()

        'スレッドtが終わるまで待機する
        t.Join()

        '結果を表示する
        Console.WriteLine(sample.ReturnValue)

        Console.ReadLine()
    End Sub
End Class
}}

#code(csharp){{
//マルチスレッドメソッドのためのクラス
class SampleClass
{
    //スレッドが返す値
    public int ReturnValue;
    //スレッドに渡す値
    public int SleepTime;

    //別スレッドで実行するメソッド
    public void MyThread()
    {
        int start = System.Environment.TickCount;

        //長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始");
        //sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(SleepTime);
        //処理が終わったことを知らせる
        Console.WriteLine("終わりました");

        //実際にかかった時間を返す
        ReturnValue = System.Environment.TickCount - start;
    }
}

class MainClass
{
    //エントリポイント
    public static void Main()
    {
        //SampleClassオブジェクトの作成
        SampleClass sample = new SampleClass();
        //スレッドに渡すデータを設定する
        sample.SleepTime = 10000;

        //Threadオブジェクトを作成する
        System.Threading.Thread t = 
            new System.Threading.Thread(
                new System.Threading.ThreadStart(sample.MyThread));
        //スレッドを開始する
        t.Start();

        //スレッドtが終わるまで待機する
        t.Join();

        //結果を表示する
        Console.WriteLine(sample.ReturnValue);

        Console.ReadLine();
    }
}
}}

別スレッドから戻り値を取得するための方法としては、コールバックメソッドを使う方法もあります。この場合は上記の例のようにJoinメソッドでスレッドの終了を待機する必要がありません。この方法では、データを渡すスレッドからデータを受け取るスレッドのメソッドをデリゲート(またはイベント)により呼び出し、その引数として結果を渡します。

次の例は上記のコードをさらに書き換え、別スレッドで実行されるSpendLongTimeメソッドからコールバックメソッドGetCallbackResultを呼び出すことにより結果を返すようにしています。なお当然ですが、GetCallbackResultはメインスレッドではなく、別スレッドで実行されることに注意してください。

#code(vbnet){{
'マルチスレッドメソッドのためのクラス
Class SampleClass
    'スレッドが結果を返すためのコールバックデリゲート
    Delegate Sub MyThreadCallback(ByVal returnValue As Integer)
    Private callbackDelegate As MyThreadCallback
    'スレッドに渡す値
    Private sleepTime As Integer

    'コンストラクタ
    Public Sub New(ByVal time As Integer, _
            ByVal callback As MyThreadCallback)
        'コンストラクタでデータを受け取る
        sleepTime = time
        callbackDelegate = callback
    End Sub

    '別スレッドで実行するメソッド
    Public Sub MyThread()
        Dim start As Integer = System.Environment.TickCount

        '長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始")
        'sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(sleepTime)
        '処理が終わったことを知らせる
        Console.WriteLine("終わりました")

        'コールバックデリゲートを実行して結果を返す
        If Not (callbackDelegate Is Nothing) Then
            callbackDelegate((System.Environment.TickCount - start))
        End If
    End Sub
End Class

Class MainClass
    'エントリポイント
    Public Shared Sub Main()
        'SampleClassオブジェクトの作成
        '同時にデータを渡す
        Dim sample As New SampleClass(10000, _
            New SampleClass.MyThreadCallback(AddressOf GetCallbackResult))

        'Threadオブジェクトを作成する
        Dim t As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf sample.MyThread))
        'スレッドを開始する
        t.Start()

        Console.ReadLine()
    End Sub

    'スレッド終了時に呼び出されるコールバック関数
    Private Shared Sub GetCallbackResult(ByVal returnValue As Integer)
        '結果を表示する
        Console.WriteLine(returnValue)
    End Sub
End Class
}}

#code(csharp){{
//マルチスレッドメソッドのためのクラス
class SampleClass
{
    //スレッドが結果を返すためのコールバックデリゲート
    public delegate void MyThreadCallback(int returnValue);
    private MyThreadCallback callbackDelegate;
    //スレッドに渡す値
    private int sleepTime;

    //コンストラクタ
    public SampleClass(int time, MyThreadCallback callback)
    {
        //コンストラクタでデータを受け取る
        sleepTime = time;
        callbackDelegate = callback;
    }

    //別スレッドで実行するメソッド
    public void MyThread()
    {
        int start = System.Environment.TickCount;

        //長い時間のかかる処理があるものとする
        Console.WriteLine("スレッド開始");
        //sleepTimeミリ秒停止する
        System.Threading.Thread.Sleep(sleepTime);
        //処理が終わったことを知らせる
        Console.WriteLine("終わりました");

        //コールバックデリゲートを実行して結果を返す
        if (callbackDelegate != null)
            callbackDelegate(System.Environment.TickCount - start);
    }
}

class MainClass
{
    //エントリポイント
    public static void Main()
    {
        //SampleClassオブジェクトの作成
        //同時にデータを渡す
        SampleClass sample = new SampleClass(10000, 
            new SampleClass.MyThreadCallback(GetCallbackResult));

        //Threadオブジェクトを作成する
        System.Threading.Thread t = 
            new System.Threading.Thread(
                new System.Threading.ThreadStart(sample.MyThread));
        //スレッドを開始する
        t.Start();

        Console.ReadLine();    
    }

    //スレッド終了時に呼び出されるコールバック関数
    private static void GetCallbackResult(int returnValue)
    {
        //結果を表示する
        Console.WriteLine(returnValue);
    }
}
}}

**コメント [#l95f8bad]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2004-01-27 (火) 06:00:00,DOBON!,2010-03-21 (日) 01:16:22,DOBON!)

[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]