VB2008の最近のブログ記事

前回は AsyncWaitHandle.WaitOne メソッドで非同期デリゲート処理の終了待ちをしましたので、今回は、IsCompleted プロパティで終了待ちを試してみる事に。(UIサービスを提供するスレッド向き?)

★元ネタ-マイクロソフトドキュメントで、ほぼそのままのソース。


<注意点>

・非同期呼び出しの完了を IsCompleted でポーリングすると、スレッドプールで非同期処理を実行しながら、呼出し元スレッドを実行継続できます。

・何れの終了待ちをするにしても、EndInvokeを常に呼び出して、非同期呼出しの完了とします。 ← 重要!

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

Button3 thread 10 does some work.
Wait Test begins.
.....
.....
.....
.....
.....
.....
.....
.....
The call executed on thread 7, with return value "Wait time was 2000.".

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form1

    Private Delegate Function dlgtAsyncWaitChk(ByVal tm As Integer, ByRef id As Integer) As String

    Private Function waitTest(ByVal term As Integer, ByRef threadId As Integer) As String
        Debug.Print("Wait Test begins.")
        System.Threading.Thread.Sleep(term)
        threadId = System.Threading.Thread.CurrentThread.ManagedThreadId
        Return String.Format("Wait time was {0}.", term.ToString())
    End Function

    Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
        Dim threadId As Integer
        Dim awc As dlgtAsyncWaitChk
        Dim result As IAsyncResult
        Dim retValue As String

        awc = New dlgtAsyncWaitChk(AddressOf waitTest)
        result = awc.BeginInvoke(2000, threadId, Nothing, Nothing)

        Debug.Print("Button3 thread {0} does some work.", System.Threading.Thread.CurrentThread.ManagedThreadId)

        While Not result.IsCompleted
            System.Threading.Thread.Sleep(250)
            Debug.Print(".....")
        End While

        retValue = awc.EndInvoke(threadId, result)
        Debug.Print("The call executed on thread {0}, with return value ""{1}"".", threadId, retValue)
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

前回は EndInvoke メソッドで非同期デリゲート処理の終了待ちをしましたので、今回は、AsyncWaitHandle プロパティの WaitOne メソッドで終了待ちを試してみる事に。(当然、UIスレッドには不向き)。

★元ネタ-マイクロソフトドキュメントで、ほぼそのままのソース。


<注意点>

・WaitHandle は非同期呼び出しが完了すると通知されるので、WaitOne 呼び出しでこれを待機します。

・待機ハンドルの使用終了と同時にシステムリソースを解放する場合、WaitHandle.Close メソッドを呼び出します。

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

Button2 thread 10 does some work.
Wait Test begins.
The call executed on thread 7, with return value "Wait time was 2000.".

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form1

    Private Delegate Function dlgtAsyncWaitChk(ByVal tm As Integer, ByRef id As Integer) As String

    Private Function waitTest(ByVal term As Integer, ByRef threadId As Integer) As String
        Debug.Print("Wait Test begins.")
        System.Threading.Thread.Sleep(term)
        threadId = System.Threading.Thread.CurrentThread.ManagedThreadId
        Return String.Format("Wait time was {0}.", term.ToString())
    End Function

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        Dim threadId As Integer
        Dim awc As dlgtAsyncWaitChk
        Dim result As IAsyncResult
        Dim retValue As String

        awc = New dlgtAsyncWaitChk(AddressOf waitTest)
        result = awc.BeginInvoke(2000, threadId, Nothing, Nothing)

        Debug.Print("Button2 thread {0} does some work.", System.Threading.Thread.CurrentThread.ManagedThreadId)

        result.AsyncWaitHandle.WaitOne()

        retValue = awc.EndInvoke(threadId, result)

        result.AsyncWaitHandle.Close() ' ガベージコレクションの効率化
        Debug.Print("The call executed on thread {0}, with return value ""{1}"".", threadId, retValue)
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

非同期デリゲート処理の呼び出し完了待ちを検索していたら、マイクロソフトが言うには、

・呼び出すメソッドと同じシグネチャを持つデリゲートを定義すれば、.NETでは全てのメソッドを非同期的に呼び出すことが出来る。

・デリゲート定義では、共通言語ランタイム(CLR)により適切なシグネチャが使用された BeginInvoke および EndInvoke メソッドが自動的に定義される。

・BeginInvoke メソッドは、非同期的に実行するメソッドと同じパラメーターと、2つの省略可能な追加パラメーターを持っている。
 追加の1つ目は、非同期呼び出しが完了したときに呼び出されるメソッドを参照する AsyncCallback デリゲート
 追加の2つ目は、コールバックメソッドに情報を渡すユーザー定義オブジェクト。

・BeginInvoke は IAsyncResult を返すので、これを使用して非同期呼び出しの進捗状況を監視出来る。

・EndInvoke メソッドは、非同期処理の呼び出し結果を取得し、BeginInvoke の後であればいつでも呼び出すことが出来る。

・EndInvoke のパラメーターには、非同期実行するメソッドの <Out>ByRef と ByRef、BeginInvoke の戻り値 IAsyncResult が含まれる。

らしいので、追加パラメーター2つを省略して(=Nothing)、EndInvokeメソッドで非同期処理の終了待ちを試してみる事に(当然、UIスレッドには不向き)。

★元ネタ-マイクロソフトドキュメントで、ほぼそのままのソース。


<注意点>

・非同期呼び出しが完了していない場合、 EndInvoke は非同期呼び出しが完了するまで呼び出し元スレッドをブロック。

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

Wait Test begins.
Button1 thread 10 does some work.
The call executed on thread 7, with return value "Wait time was 2000.".

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form1

    Private Delegate Function dlgtAsyncWaitChk(ByVal tm As Integer, ByRef id As Integer) As String

    Private Function waitTest(ByVal term As Integer, ByRef threadId As Integer) As String
        Debug.Print("Wait Test begins.")
        System.Threading.Thread.Sleep(term)
        threadId = System.Threading.Thread.CurrentThread.ManagedThreadId
        Return String.Format("Wait time was {0}.", term.ToString())
    End Function

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim threadId As Integer
        Dim awc As dlgtAsyncWaitChk
        Dim result As IAsyncResult
        Dim retValue As String

        awc = New dlgtAsyncWaitChk(AddressOf waitTest)
        result = awc.BeginInvoke(2000, threadId, Nothing, Nothing)

        Debug.Print("Button1 thread {0} does some work.", System.Threading.Thread.CurrentThread.ManagedThreadId)

        retValue = awc.EndInvoke(threadId, result)
        Debug.Print("The call executed on thread {0}, with return value ""{1}"".", threadId, retValue)
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

フリーズしてる訳でもないんだから、プログレスバーでも出しておけば良いんじゃないの?と書いておきながら、非同期処理でUIにプログレスバーを表示してみる事に。

矢張り曲者は、別スレッドからのUIコントロール更新でしょうか。(デリゲートメソッドもコールバックメソッドも、スレッドプールで処理を実行)

【デリゲートメソッドの終了待ち】

・BeginInvoke呼出しメソッドの終了をEndInvokeで受けるのですが、UI(メイン)スレッド中に EndInvoke を呼び出すと非同期(デリゲート)メソッドが完了するまで、呼出し元スレッドがブロックされてしまいます。*1

・UIサービスを提供するスレッドからは、EndInvoke を呼び出さない方が宜しいかと。⇒ コールバックメソッドの利用

*1:IDEのデバッグ実行では、飛んだまま戻ってこず


<注意点>

・仮引数の範囲のチェック(0%~100%のプログレスバー設定値)や何処でチェックするかなどは、多分に好みの問題だと思うので・・・。

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

・ボタン1を押すと、

プログレスバー表示その1

プログレスバー表示その2

・ボタン2を押すと、

プログレスバー表示その3

プログレスバー表示その4

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form4

    Private Delegate Sub dlgtWork1Prgbar1(ByVal data As Integer)

    Private Sub Set1Prgbar1Value(ByVal percent As Integer)
        If InvokeRequired = True Then
            Invoke(New dlgtWork1Prgbar1(AddressOf Set1Prgbar1Value), percent)
        Else
            ProgressBar1.Value = percent
            If percent >= 100 Then
                Label1.ForeColor = System.Drawing.Color.Red
                Label1.Text = "完了"
                Button1.Enabled = True
            End If
        End If
    End Sub

    Private Sub work1PrgBar1(ByVal no As Integer)
        For i As Integer = no To 10
            System.Threading.Thread.Sleep(500) ' 時間の掛かる処理の代替
            Set1Prgbar1Value(i * 10)
        Next
    End Sub

    Private Sub bar1Callback1(ByVal ar As IAsyncResult)
        Dim arWk1Prgbar1 As dlgtWork1Prgbar1

        arWk1Prgbar1 = CType(ar.AsyncState, dlgtWork1Prgbar1)
        arWk1Prgbar1.EndInvoke(ar)
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim wk1PrgBar1 As dlgtWork1Prgbar1
        Dim ar As IAsyncResult

        Button1.Enabled = False
        Label1.ForeColor = System.Drawing.Color.FromKnownColor(KnownColor.ControlText)
        Label1.Text = "実行中・・・"

        Set1Prgbar1Value(0)
        wk1PrgBar1 = New dlgtWork1Prgbar1(AddressOf work1PrgBar1)
        ar = wk1PrgBar1.BeginInvoke(1, New AsyncCallback(AddressOf bar1Callback1), wk1PrgBar1)
    End Sub

/*--------------------------  お試しソース 改 -----------------------------*/

    Structure prgrsData
        Dim percent As Integer
        Dim leftTm As TimeSpan
    End Structure

    Private Delegate Sub dlgtWork2Prgbar1(ByVal data As prgrsData)

    Private Sub Set2Prgbar1Value(ByVal prgDat As prgrsData)
        If InvokeRequired = True Then
            Invoke(New dlgtWork2Prgbar1(AddressOf Set2Prgbar1Value), prgDat)
        Else
            ProgressBar1.Value = prgDat.percent
            Label1.Text = String.Format("あと {0:0.0} 秒・・・", prgDat.leftTm.TotalSeconds)
            If prgDat.percent >= 100 Then
                Label1.ForeColor = System.Drawing.Color.Red
                Label1.Text = "完了"
                Button2.Enabled = True
            End If
        End If
    End Sub

    Private Sub work2PrgBar1(ByVal prgDat As prgrsData)
        Const Cntr As Integer = 10
        Const MSec As Integer = 500
        Dim pData As prgrsData

        For i As Integer = prgDat.percent To Cntr
            System.Threading.Thread.Sleep(MSec) ' 時間の掛かる処理の代替
            pData.percent = i * Cntr
            pData.leftTm = TimeSpan.FromMilliseconds((Cntr - i) * MSec)
            Set2Prgbar1Value(pData)
        Next
    End Sub

    Private Sub bar1Callback2(ByVal ar As IAsyncResult)
        Dim arWk2Prgbar1 As dlgtWork2Prgbar1

        arWk2Prgbar1 = CType(ar.AsyncState, dlgtWork2Prgbar1)
        arWk2Prgbar1.EndInvoke(ar)
    End Sub

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        Dim wk2Prgbar1 As dlgtWork2Prgbar1
        Dim ar As IAsyncResult
        Dim progDat As prgrsData

        Button2.Enabled = False
        Label1.ForeColor = System.Drawing.Color.FromKnownColor(KnownColor.ControlText)
        Label1.Text = "実行中・・・"

        progDat.percent = 0
        progDat.leftTm = TimeSpan.FromMilliseconds(5000)
        Set2Prgbar1Value(progDat)
        progDat.percent = 1
        wk2Prgbar1 = New dlgtWork2Prgbar1(AddressOf work2PrgBar1)
        ar = wk2Prgbar1.BeginInvoke(progDat, New AsyncCallback(AddressOf bar1Callback2), wk2Prgbar1)
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

前回の非同期処理では、別スレッドからUIコントロールの Label1.Text 更新が出来ませんでしたので、別スレッドからUIコントロール更新の仕方を浚ってみる事に。

別スレッドで実行しているメソッドがUIを書き換える場合、処理をUIスレッドに移行する必要が有りますが、それにはWindowsフォームが備えているInvokeメソッドを呼び出すと良い様です。

尚、別スレッドで実行されると分かっているなら、直接Invokeメソッドを呼び出した方が簡単なのですが、後の改修などを考慮してInvokeRequiredプロパティを使用するのが定石?なんだそうです。


<注意点>

・InvokeRequiredプロパティは、UIスレッドと異なるスレッドで実行していると"真"を返します。

・分かり易い?様に敢えて、別スレッドからUIを更新する為のデリゲートを定義していますが、(関数呼び出しとは違い)デリゲートの名称は区別されず、同一のシグネチャが重要なので、「Invoke(New dlgtShowMsg(AddressOf SetLabel1Text), ・・・)」でも構いません。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

・メッセージボックスの表示は、前回と同じなので省略

・ボタン3を押すと、約2秒後にラベルのテキストを表示

ラベルの更新テキストを表示

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form3

    Delegate Sub dlgtShowMsg(ByVal msg As String)

    Private Sub ShowMessage(ByVal msg As String) ' BeginInvokeから呼ばれるので別スレッド
        System.Threading.Thread.Sleep(2000)
        Invoke(New dlgtSetLabel1(AddressOf SetLabel1Text), "Asynchronous Method" & " and " & msg) ' 別スレッドからUIを更新するInvoke呼出し
        MsgBox(msg, MsgBoxStyle.Information, "Asynchronous Method")
    End Sub

    Private Sub showCallback(ByVal ar As IAsyncResult) ' コールバックメソッド
        Dim arShowMsg As dlgtShowMsg

        arShowMsg = CType(ar.AsyncState, dlgtShowMsg)
        arShowMsg.EndInvoke(ar)
    End Sub

    Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
        Dim showMsg As dlgtShowMsg
        Dim ar As IAsyncResult

        showMsg = New dlgtShowMsg(AddressOf ShowMessage)
        ar = showMsg.BeginInvoke("See actions", New AsyncCallback(AddressOf showCallback), showMsg)

        MsgBox("See actions", MsgBoxStyle.Information, "Asynchronous Test")
    End Sub

    Delegate Sub dlgtSetLabel1(ByVal msg As String) ' ①別スレッドからUIを更新するデリゲートの定義

    Private Sub SetLabel1Text(ByVal msg As String) ' ②別スレッドからUIを更新するメソッドの作成
        If InvokeRequired = True Then ' ③Windowsフォームは別スレッドからのUI操作は保障されていない
            Invoke(New dlgtSetLabel1(AddressOf SetLabel1Text), "Required " & msg)
        Else
            Label1.Text = msg
        End If
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

前回、デリゲートの機能の一つを浚いましたので、今回はDelegateの二つ目の機能である「非同期処理」について浚ってみる事に。

色々と調べてみると、VBコンパイラはDelegateを宣言すると、自動的にBeginInvokeメソッドとEndInvokeメソッドを生成してくれます。

UIがフリーズしないようにする為の非同期処理は、この二つのメソッドにより可能となる様です。

【非同期呼出しの勘所】

・呼び出し方:BeginInvoke(デリゲートの仮引数 *1、コールバックデリゲート、Object) As IAsyncResult

・BeginInvoke メソッドを使用する事により、デリゲート経由で呼び出すメソッドを別のスレッドで非同期に実行。

・コールバックメソッドの中で EndInvoke メソッドを呼び出し、メモリリークの可能性を回避。

・デリゲート経由でメソッドを呼び出すと、CLRが管理しているスレッドプールに実行を依頼。その後、制御が直ぐに戻ってくるので呼び出したメソッドとは関係無く、(終了を待たずに)プログラムを先に進める事が可能。

・コールバックメソッドはスレッドプール側で実行されるので、UIを直接更新する事は不可。

*1:引数の数はデリゲート宣言の仮引数に基づくので、お試しソースでは一つ。


<注意点>

・メソッドのシグネチャは同一である事が重要。コールバックメソッドも対応するデリゲートの引数の数と型、戻り値の型に合わせる事。

・BeginInvoke のデリゲート呼出しは非同期で行われる(別スレッド)ので、メソッド呼出し後、直ぐに呼び出し元スレッドに制御が戻る。今回の場合、メッセージボックスのタイトルは「Asynchronous Test」「Asynchronous Method」の順で表示される。

・Windowsフォームは、別スレッドからのUI操作は保障されていないので、②の Label1.Text のコメントを外して実行すると、「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Label1' がアクセスされました。」と例外が発生する。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

・ボタン2を押すと、直ぐに表示

Asynchronous Test のタイトルで表示

・その約2秒後に表示

Asynchronous Method のタイトルで表示

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form3

    Delegate Sub dlgtDispRpt(ByVal msg As String) ' ①非同期 デリゲートの定義

    Private Sub DisplayRpt(ByVal msg As String) ' ②非同期 デリゲートで呼び出すメソッドの作成
        System.Threading.Thread.Sleep(2000)
        '        Label1.Text = "Asynchronous Method" & " and " & msg
        MsgBox(msg, MsgBoxStyle.Information, "Asynchronous Method")
    End Sub

    Private Sub dispCallback(ByVal ar As IAsyncResult) ' コールバックメソッド
        Dim arDispRpt As dlgtDispRpt

        arDispRpt = CType(ar.AsyncState, dlgtDispRpt)
        arDispRpt.EndInvoke(ar)
    End Sub

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        Dim dispRpt As dlgtDispRpt ' ③非同期 デリゲート型の変数を宣言
        Dim ar As IAsyncResult

        dispRpt = New dlgtDispRpt(AddressOf DisplayRpt) ' ④非同期 デリゲート型変数にメソッドを登録
        ar = dispRpt.BeginInvoke("See actions", New AsyncCallback(AddressOf dispCallback), dispRpt) ' ⑤非同期 デリゲートの呼び出し

        MsgBox("See actions", MsgBoxStyle.Information, "Asynchronous Test")
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

UIは元よりUXは尚更、快適な操作環境を云々、と叫ばれてから早十余年。タッチ操作が主流?となった今では、当然の如き言われ様。

でもね、フリーズしてる訳でもないんだし、プログレスバーでも出しておけば良いんじゃないの?と思ってしまうのはさて置き。

色々検索してみると、即応性=非同期 ⇒ コールバック ⇔ デリゲート が良いらしい(単なるマイクロソフトの推奨?)って事で、

以前、C言語でデリゲートとイベントの概要記事「VC++フォームだからこそ、コマンドプロンプトでEXE?」を書きましたので、それを眺めつつ先ずは、Delegateの使い方を浚ってみる事に。

マイクロソフトのドキュメントによると、

デリゲートは、メソッドを参照するオブジェクトです。デリゲートは他のプログラミング言語で使用される関数ポインターに似ているため、"タイプセーフ関数ポインター"と説明されることがあります。しかしながら、関数ポインターとは異なり、Visual Basicのデリゲートは、System.Delegateクラスに基づく参照型です。

と、何とも要領の得ない、教えたくない様な説明。きっと独自では使って欲しくないのでしょうね。

マイクロソフトのドキュメントによれば、Delegateの構文は、

[<attrlist>][accessmodifier][Shadows] Delegate [Sub | Function] name [(Of typeparamlist)][([parameterlist])][As type]

なので、敢えて大雑把に言ってしまえば以下の様な形式かと。

Delegate Sub デリゲート名称(仮引数リスト...)

または

Delegate Function デリゲート名称(仮引数リスト...) As 戻り値の型

★さて、デリゲートとは?と問われれば一種の型です。利用するためのポイントは以下の5つで、

①デリゲートの宣言(メソッドのシグネチャを定義)
②デリゲートで呼び出すメソッドの作成
③デリゲート型の変数を宣言
④デリゲート型変数にメソッドを登録
⑤デリゲートの呼出し

となるようです。


<注意点>

・デリゲートの機能の一つは、メソッドのアドレスを格納する変数の宣言と呼び出すメソッドを登録出来る型。

・メソッドのシグネチャは同一である事が重要。引数の数と型、戻り値の型が同じ。

・通常のデリゲートの呼出しは同期して行われる(同一スレッド)ので、メソッドの処理が完了してから呼び出し元スレッドに制御が戻る。今回の場合、メッセージボックスのタイトルは「Delegate Method」「Delegate Test」の順で表示される。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*-------------------------------- お試し結果 ------------------------------*/

・ボタン1を押すと、約2秒後に表示

Delegate Method のタイトルで表示

・上記画面のOKボタンを押すと、直ぐに表示

Delegate Test のタイトルで表示

・ボタン1を押すと、約2秒後にラベルのテキストを表示

ラベルの更新テキストを表示

/*----------------------------------------------------------------------------*/


/*----------------------------  お試しソース -------------------------------*/

Public Class Form3

    Delegate Sub dlgtDispMsg(ByVal msg As String) ' ①デリゲートの定義

    Private Sub DisplayMsg(ByVal msg As String) ' ②デリゲートで呼び出すメソッドの作成
        System.Threading.Thread.Sleep(2000)
        Label1.Text = "Delegate Method" & " and " & msg
        MsgBox(msg, MsgBoxStyle.Information, "Delegate Method")
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim dispMsg As dlgtDispMsg ' ③デリゲート型の変数を宣言

        dispMsg = New dlgtDispMsg(AddressOf DisplayMsg) ' ④デリゲート型変数にメソッドを登録
        dispMsg("See actions") ' ⑤デリゲートの呼び出し

        MsgBox("See actions", MsgBoxStyle.Information, "Delegate Test")
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

WindowsXPやWindows7とVisualBasic2008でガサゴソやっていると、ますます周回遅れが嵩んで追い付けなくなると言うか、浦島次郎にさえ成れない? という事で、非同期ソケット通信を試してみる事に。

凡そ10年前、Windows 8/RT 以降の Windows Store App では、時間のかかるAPIには非同期バージョンの物しか用意されていない。と言われていましたが、最新のWindowsでは如何なんでしょうか?

同期バージョンのAPIが多数復活(?)してたら、喜びも一塩なんですけれどもね。


<注意点>

・マイクロソフトが言うには、

非同期ソケットは、システムスレッドプールの複数のスレッドを使用して、データの送受信接続を処理します。1つ目はネットワーク接続を処理するスレッド、2つ目はデータの送信または受信の開始を処理するスレッド、他のスレッドはデータの送信または受信を処理するスレッドなどがあります。

らしいので、非同期処理のポイントは「コールバック・デリゲート」で、元々、マルチスレッド処理の事だったのかも?

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*---- サーバー側 -------------------- お試し結果 --------------------------*/

Waiting for a connection...
Waiting for a connection...
Read 48 bytes from socket.
Data : This is a test.
Asynchronous communications<EOF>
Sent 48 bytes to client.

/*----------------------------------------------------------------------------*/

/*---- クライアント側 ---------------- お試し結果 --------------------------*/

Socket connected to ::1:22000
Sent 15 bytes to server.
Sent 33 bytes to server.
Response received : This is a test.
Asynchronous communications<EOF>

/*----------------------------------------------------------------------------*/


/*---- サーバー側 ------------------  お試しソース -------------------------*/

'Imports System.Net
'Imports System.Net.Sockets
'Imports System.Text

Public Class Form1

    ' 非同期サーバー受信用状態オブジェクト
    Public Class srvStateObj
        ' データ送受信ソケット
        Public workSocket As Net.Sockets.Socket = Nothing
        ' バッファサイズ
        Public Const BufferSize As Integer = 1024
        ' 受信バッファ
        Public buffer(BufferSize) As Byte
        ' 受信データ文字列
        Public sb As New System.Text.StringBuilder
    End Class 'srvStateObj

    ' イベント待ちハンドラ シグナル設定
    Private acceptDone As New Threading.ManualResetEvent(False)

    Private Sub srvSendCallback(ByVal ar As IAsyncResult)

        Dim cpWkDatSkt As Net.Sockets.Socket ' データ送受信ソケットのコピー
        Dim sndLen As Integer

        ' データ送受信ソケットの取得
        cpWkDatSkt = CType(ar.AsyncState, Net.Sockets.Socket)

        ' データ送信終了
        sndLen = cpWkDatSkt.EndSend(ar)
        Debug.Print("Sent {0} bytes to client.", sndLen)

        cpWkDatSkt.Shutdown(Net.Sockets.SocketShutdown.Both)
        cpWkDatSkt.Close()
    End Sub ' srvSendCallback

    Private Sub srvSend(ByVal wkDatSkt As Net.Sockets.Socket, ByVal data As String)

        Dim byteData(1024) As Byte

        ' 文字列データをバイト列データに変換
        byteData = System.Text.Encoding.ASCII.GetBytes(data)

        ' データ送信開始
        wkDatSkt.BeginSend(byteData, 0, byteData.Length, 0, New AsyncCallback(AddressOf srvSendCallback), wkDatSkt)
    End Sub ' srvSend

    Private Sub ReadCallback(ByVal ar As IAsyncResult)

        Dim state As srvStateObj
        Dim wkDatSkt As Net.Sockets.Socket ' データ送受信ソケットのコピー

        Dim rdLen As Integer
        Dim msg As String

        ' データ送受信ソケットの取得
        state = CType(ar.AsyncState, srvStateObj)
        wkDatSkt = state.workSocket

        ' データ受信終了
        rdLen = wkDatSkt.EndReceive(ar)

        If rdLen > 0 Then
            ' ストリングビルダーに受信データを格納
            state.sb.Append(System.Text.Encoding.ASCII.GetString(state.buffer, 0, rdLen))

            ' データ受信終了をチェック
            msg = state.sb.ToString()
            If msg.IndexOf("<EOF>") > -1 Then
                ' 全データ受信終了
                Debug.Print("Read {0} bytes from socket. " + vbLf + "Data : {1}", msg.Length, msg)
                ' クライアントへデータ送信(エコーバック)
                srvSend(wkDatSkt, msg)
            Else
                ' 未受信データを受信
                wkDatSkt.BeginReceive(state.buffer, 0, srvStateObj.BufferSize, 0, New AsyncCallback(AddressOf ReadCallback), state)
            End If
        End If
    End Sub ' ReadCallback

    Private Sub AcceptCallback(ByVal ar As IAsyncResult)

        Dim cpSrvSkt As Net.Sockets.Socket ' サーバー側ソケットのコピー
        Dim datSkt As Net.Sockets.Socket ' クライアント側とのデータ送受信ソケット

        Dim state As srvStateObj

        ' サーバーソケットの取得
        cpSrvSkt = CType(ar.AsyncState, Net.Sockets.Socket)
        ' 接続要求許可受付終了
        datSkt = cpSrvSkt.EndAccept(ar)

        ' イベント完了をセット(メイン処理を続行するため)
        acceptDone.Set()

        ' 非同期受信用状態オブジェクト初期化とデータ送受信ソケット格納
        state = New srvStateObj
        state.workSocket = datSkt
        datSkt.BeginReceive(state.buffer, 0, srvStateObj.BufferSize, 0, New AsyncCallback(AddressOf ReadCallback), state)
    End Sub ' AcceptCallback

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim srvSkt As Net.Sockets.Socket ' サーバー側ソケット
        Dim port As Integer

        Dim ipHostInfo As Net.IPHostEntry
        Dim ipAddress As Net.IPAddress ' ローカルIPアドレス
        Dim localEP As Net.IPEndPoint

        ' ローカルエンドポイント作成
        port = 22000
        ipHostInfo = Net.Dns.GetHostEntry(Net.Dns.GetHostName())
        ipAddress = ipHostInfo.AddressList(0)
        localEP = New Net.IPEndPoint(ipAddress, port)

        ' ソケット作成
        srvSkt = New Net.Sockets.Socket(ipAddress.AddressFamily, Net.Sockets.SocketType.Stream, Net.Sockets.ProtocolType.Tcp)

        Try
            ' ソケットとローカルエンドポイントの関連付けと、接続要求のリッスン
            srvSkt.Bind(localEP)
            srvSkt.Listen(100)

            While True
                ' イベント状態をリセット
                acceptDone.Reset()

                ' 非同期の接続要求受付開始
                Debug.Print("Waiting for a connection...")
                srvSkt.BeginAccept(New AsyncCallback(AddressOf AcceptCallback), srvSkt)

                ' 接続要求受付まで待ち(ループ継続前の処理がここに在っても良いかも?)
                acceptDone.WaitOne()
            End While

        Catch ex As Exception
            Debug.Print(ex.ToString())
        End Try
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*---- クライアント側 --------------  お試しソース -------------------------*/

'Imports System.Net
'Imports System.Net.Sockets
'Imports System.Text

Public Class Form1

    ' 非同期クライアント受信用状態オブジェクト
    Public Class clntStateObj
        ' クライアント側ソケット
        Public workSocket As Net.Sockets.Socket = Nothing
        ' バッファサイズ
        Public Const BufferSize As Integer = 256
        ' 受信バッファ
        Public buffer(BufferSize) As Byte
        ' 受信データ文字列
        Public sb As New System.Text.StringBuilder
    End Class 'clntStateObj

    ' イベント待ちハンドラ シグナル設定
    Private connectDone As New Threading.ManualResetEvent(False)
    Private sendDone As New Threading.ManualResetEvent(False)
    Private receiveDone As New Threading.ManualResetEvent(False)

    ' サーバーからの応答データ設定
    Private response As String = String.Empty

    Private Sub ReceiveCallback(ByVal ar As IAsyncResult)

        Dim state As clntStateObj
        Dim wkClntSkt As Net.Sockets.Socket ' クライアントソケットのコピー

        Dim rcvLen As Integer

        Try
            ' クライアントソケットを取得
            state = CType(ar.AsyncState, clntStateObj)
            wkClntSkt = state.workSocket

            ' データ受信終了
            rcvLen = wkClntSkt.EndReceive(ar)

            If rcvLen > 0 Then
                ' ストリングビルダーに受信データを格納
                state.sb.Append(System.Text.Encoding.ASCII.GetString(state.buffer, 0, rcvLen))

                ' 残りのデータを取得
                wkClntSkt.BeginReceive(state.buffer, 0, clntStateObj.BufferSize, 0, New AsyncCallback(AddressOf ReceiveCallback), state)
            Else
                ' 全データ受信
                If state.sb.Length > 1 Then
                    response = state.sb.ToString()
                End If
                ' 全データ受信完了イベントセット(メイン処理を続行するため)
                receiveDone.Set()
            End If

        Catch ex As Exception
            Debug.Print(ex.ToString())
        End Try
    End Sub ' ReceiveCallback

    Private Sub Receive(ByVal clntSkt As Net.Sockets.Socket)

        Dim state As clntStateObj

        ' 非同期受信用のオブジェクトの初期化とクライアントソケット格納
        state = New clntStateObj
        state.workSocket = clntSkt

        ' データ受信開始
        clntSkt.BeginReceive(state.buffer, 0, clntStateObj.BufferSize, 0, New AsyncCallback(AddressOf ReceiveCallback), state)
    End Sub ' Receive

    Private Sub clntSendCallback(ByVal ar As IAsyncResult)

        Dim cpClntSkt As Net.Sockets.Socket ' クライアントソケットのコピー
        Dim sndLen As Integer

        Try
            ' クライアントソケットを取得
            cpClntSkt = CType(ar.AsyncState, Net.Sockets.Socket)

            ' 送信完了
            sndLen = cpClntSkt.EndSend(ar)
            Debug.Print("Sent {0} bytes to server.", sndLen)

            ' 送信完了イベントセット(メイン処理を続行するため)
            sendDone.Set()

        Catch ex As Exception
            Debug.Print(ex.ToString())
        End Try
    End Sub ' clntSendCallback

    Private Sub clntSend(ByVal clntSkt As Net.Sockets.Socket, ByVal data As String)

        Dim byteData(1024) As Byte

        ' 文字列データをバイト列データに変換
        byteData = System.Text.Encoding.ASCII.GetBytes(data)

        ' データ送信開始
        clntSkt.BeginSend(byteData, 0, byteData.Length, 0, New AsyncCallback(AddressOf clntSendCallback), clntSkt)
    End Sub ' clntSend

    Private Sub ConnectCallback(ByVal ar As IAsyncResult)

        Dim cpClntSkt As Net.Sockets.Socket ' クライアントソケットのコピー

        Try
            ' クライアントソケットを取得
            cpClntSkt = CType(ar.AsyncState, Net.Sockets.Socket)

            ' 接続完了
            cpClntSkt.EndConnect(ar)
            Debug.Print("Socket connected to {0}", cpClntSkt.RemoteEndPoint.ToString())

            ' 接続完了イベントセット(メイン処理を続行するため)
            connectDone.Set()

        Catch ex As Exception
            Debug.Print(ex.ToString())
        End Try
    End Sub ' ConnectCallback

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim clntSkt As Net.Sockets.Socket ' クライアント側ソケット
        Dim port As Integer

        Dim ipHostInfo As Net.IPHostEntry
        Dim ipAddress As Net.IPAddress ' リモートIPアドレス
        Dim remoteEP As Net.IPEndPoint

        ' リモートエンドポイント作成
        port = 22000
        ipHostInfo = Net.Dns.GetHostEntry(Net.Dns.GetHostName())
        ipAddress = ipHostInfo.AddressList(0)
        remoteEP = New Net.IPEndPoint(ipAddress, port)

        ' ソケット作成
        clntSkt = New Net.Sockets.Socket(ipAddress.AddressFamily, Net.Sockets.SocketType.Stream, Net.Sockets.ProtocolType.Tcp)

        ' イベント状態をリセット
        connectDone.Reset()
        sendDone.Reset()
        receiveDone.Reset()

        ' 応答データをクリア
        response = String.Empty

        ' サーバーへ接続要求開始
        clntSkt.BeginConnect(remoteEP, New AsyncCallback(AddressOf ConnectCallback), clntSkt)
        ' 接続完了待ち
        connectDone.WaitOne()

        ' サーバーへデータ送信開始
        clntSend(clntSkt, "This is a test.")
        ' 送信完了待ち
        sendDone.WaitOne()

        ' 送信イベント状態をリセット
        sendDone.Reset() ' 05/29 訂正
        clntSend(clntSkt, vbLf + "Asynchronous communications<EOF>")
        sendDone.WaitOne()

        ' サーバーからの応答受信開始
        Receive(clntSkt)
        ' 受信完了待ち
        receiveDone.WaitOne()

        Debug.Print("Response received : {0}", response)

        ' ソケット解放
        clntSkt.Shutdown(Net.Sockets.SocketShutdown.Both)
        clntSkt.Close()
    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

前回のように同期ソケット通信にマルチスレッドを追加してノンブロッキングモードを実現するなら、最初っから非同期ソケット通信にしたらどうだろうと、試してみる事に。

マイクロソフトのドキュメントによると、

・非同期サーバーソケットは、.NET Frameworkの非同期プログラミングモデルを使用してネットワークサービス要求を処理します。Socketクラスは、標準の.NET Frameworkの非同期名前付けパターンに従います。例えば、同期のAcceptメソッドは非同期のBeginAcceptメソッドとEndAcceptメソッドに対応します。

非同期サーバーソケットには、ネットワークからの接続要求の受け入れを開始するメソッド、接続要求を処理してネットワークからデータの受信を開始するコールバックメソッド、データの受信を終了するコールバックメソッドが必要です。

・非同期クライアントソケットは、標準の.NET Framework非同期プログラミングモデルを使用して、1つのスレッドでネットワーク接続を処理しながら、アプリケーションは元のスレッドで実行を継続します。非同期メソッドの場合、Socketクラスは.NET Frameworkの名前付けパターンに従います。例えば、同期のReceiveメソッドは非同期のBeginReceiveメソッドとEndReceiveメソッドに対応します。

非同期操作には、操作の結果を返すコールバックメソッドが必要です。結果を知る必要がないアプリケーションの場合、コールバックメソッドは必要ありません。

非同期クライアントソケットには、ネットワークデバイスへの接続を開始するメソッドと接続を完了するコールバックメソッド、データの送信を開始するメソッドと送信を完了するコールバックメソッド、およびデータの受信を開始するメソッドとデータの受信を終了するコールバックメソッドが必要です。

・何れのコールバックメソッドもAsyncCallbackデリゲートを実装しており、voidを返してIAsyncResult型の1つのパラメーターを受け取ります。

らしいのですが、今一つピンと来ないのでSocket操作の表を作ってみました。

非同期サーバー/クライアントのソケット操作
非同期サーバーソケット非同期クライアントソケット
操作・動作 メ *1 サ *2 デ *3 操作・動作 メ *1 サ *2 デ *3
Button1_Click     Button1_Click    
BeginAccept が
AcceptCallback 呼出し
  BeginConnect が
ConnectCallback 呼出し
 
AcceptCallback     ConnectCallback    
EndAccept     EndConnect    
BeginReceive が
ReadCallback 呼出し
  Button1_Click    
ReadCallback     Send 呼出し    
EndReceive     Send    
Send 呼出し     BeginSend が
SendCallback 呼出し
 
Send (ReadCallback)     SendCallback    
BeginSend が
SendCallback 呼出し
  EndSend    
SendCallback     Button1_Click    
EndSend     Receive 呼出し    
  Receive    
BeginReceive が
ReceiveCallback 呼出し
 
ReceiveCallback    
EndReceive    
(BeginReceive が
ReceiveCallback 呼出し)
  (↑) (○)

 *1:メインスレッド
 *2:サブスレッド
 *3:AsyncCallbackのデリゲート実装を示す

でも、却って分かり辛くなってしまった感じも?
あと、非同期にすると例外処理がどう変わるのかも、今一不明(ちゃんと調べろよ!)。


<注意点>

・動作は非同期なので、ノンブロッキングモードらしいです。

前回までの動作は、サーバー側/クライアント側共にブロッキングモードだったので、マルチスレッドを利用してサーバー側の見掛け上のハングアップ(応答なし)を回避してみる事に。

(クライアント側を敢えてスレッド分けする利点が・・・)


<注意点>

・動作はスレッド利用によりノンブロッキングモードです。

・クライアント側ソースの前回との相違は、以下のみ。
    Dim message As String = "There is it!<EOF>"

・エラー処理は手抜きです。


お試し環境
  Windows7 64bit Edition
  Visual Basic 2008 AnyCPU対象


/*---- サーバー側 -------------------- お試し結果 --------------------------*/

Waiting for a connection...
Connected!
Waiting for a connection...
Received: There is it!<EOF>
Sent: THERE IS IT!<EOF>

/*----------------------------------------------------------------------------*/

/*---- クライアント側 ---------------- お試し結果 --------------------------*/

Sent: There is it!<EOF>
Received: THERE IS IT!<EOF>

/*----------------------------------------------------------------------------*/


/*---- サーバー側 ------------------  お試しソース -------------------------*/

'Imports System.Net
'Imports System.Net.Sockets
'Imports System.Text

Public Class Form1

    Private Sub subThread2(ByVal obj As Object)

        Dim clnt As Net.Sockets.TcpClient ' クライアント側ソケットのコピー
        Dim stream As Net.Sockets.NetworkStream ' クライアント側との送受信ストリーム

        ' バイト列送受信バッファ
        Dim sndBytes(1024) As Byte
        Dim rcvBytes(1024) As Byte

        Dim i As Integer
        Dim msg As String

        clnt = CType(obj, Net.Sockets.TcpClient)
        Try
            ' クライアントとの送受信ストリーム取得
            stream = clnt.GetStream()

            ' データを全て受信するまでループ
            i = stream.Read(rcvBytes, 0, rcvBytes.Length)
            While (i <> 0)
                ' 受信バイト列を文字列に変換
                msg = System.Text.Encoding.ASCII.GetString(rcvBytes, 0, i)
                Debug.Print("Received: {0}", msg)

                ' 受信データ処理
                msg = msg.ToUpper()
                sndBytes = System.Text.Encoding.ASCII.GetBytes(msg)

                ' 応答送信
                stream.Write(sndBytes, 0, sndBytes.Length)
                Debug.Print("Sent: {0}", msg)

                i = stream.Read(rcvBytes, 0, rcvBytes.Length)
            End While

            ' クライアント側ソケット解放
            stream.Close()
            clnt.Close()

        Catch ex As Net.Sockets.SocketException
            Debug.Print("SocketException : {0}", ex)
            Throw ' 例外が呼出し元に伝わるかは未確認
        Finally
            clnt.Close()
        End Try

    End Sub

    Private Sub subThread1(ByVal obj As Object)

        Dim srv As Net.Sockets.TcpListener ' リスナー側ソケットのコピー
        Dim client As Net.Sockets.TcpClient ' クライアント側ソケット

        Dim t As Threading.Thread ' スレッド

        srv = CType(obj, Net.Sockets.TcpListener)
        Try
            ' 接続要求受け取り開始
            While True
                Debug.Print("Waiting for a connection... ")

                ' 接続要求許可待ちの間、プログラムは一時停止
                client = srv.AcceptTcpClient()
                Debug.Print("Connected!")

                ' スレッド開始
                t = New Threading.Thread(AddressOf subThread2)
                t.Start(client)

                Threading.Thread.Sleep(0)
            End While

        Catch ex As Net.Sockets.SocketException
            Debug.Print("SocketException : {0}", ex)
        Finally
            srv.Stop()
        End Try

    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim server As Net.Sockets.TcpListener ' リスナー側ソケット
        Dim localAddr As Net.IPAddress ' ローカルIPアドレス
        Dim port As Integer

        Dim t As Threading.Thread ' スレッド

        Try
            ' リスナーソケット作成
            port = 22000
            localAddr = Net.IPAddress.Parse("127.0.0.1")
            server = New Net.Sockets.TcpListener(localAddr, port)

            ' クライアントからの受信接続要求リッスン開始
            server.Start()

            ' スレッド開始
            t = New Threading.Thread(AddressOf subThread1)
            t.Start(server)

            Threading.Thread.Sleep(0)

        Catch ex As Net.Sockets.SocketException
            Debug.Print("SocketException : {0}", ex)
        End Try

    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*---- クライアント側 --------------  お試しソース -------------------------*/

'Imports System.Net
'Imports System.Net.Sockets
'Imports System.Text

Public Class Form1

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim client As Net.Sockets.TcpClient ' クライアントソケット
        Dim port As Integer
        Dim stream As Net.Sockets.NetworkStream ' 送受信ストリーム

        Dim sndData(1024) As Byte ' バイト列送信バッファ
        Dim rcvData(1024) As Byte ' バイト列受信バッファ
        Dim rcvLen As Integer
        Dim msg As String

        Dim server As String = "127.0.0.1"
        Dim message As String = "There is it!<EOF>"

        Try
            ' クライアントソケット作成
            port = 22000
            client = New Net.Sockets.TcpClient(server, port)

            ' 送信文字列をバイト配列に変換
            sndData = System.Text.Encoding.ASCII.GetBytes(message)

            ' 送受信ストリームを取得
            stream = client.GetStream()

            ' サーバーにメッセージ送信
            stream.Write(sndData, 0, sndData.Length)
            Debug.Print("Sent: {0}", message)

            ' 応答データ受信
            rcvLen = stream.Read(rcvData, 0, rcvData.Length)
            msg = System.Text.Encoding.ASCII.GetString(rcvData, 0, rcvLen)
            Debug.Print("Received: {0}", msg)

            ' ソケット解放
            stream.Close()
            client.Close()

        Catch ex As ArgumentNullException
            Debug.Print("ArgumentNullException: {0}", ex)
        Catch ex As Net.Sockets.SocketException
            Debug.Print("SocketException: {0}", ex)
        End Try

    End Sub

End Class

/*----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/*============================================================================*/

このアーカイブについて

このページには、過去に書かれたブログ記事のうちVB2008カテゴリに属しているものが含まれています。

前のカテゴリはFFFTPです。

次のカテゴリはVC++2008x64です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

お気に入りリンク

NOP法人 アジアチャイルドサポート 最も大切なボランティアは、自分自身が一生懸命に生きること