VB のたまご

作成日: 2015/12/21, 更新日: 2015/12/21


アプリケーションドメインは、空間イメージとしてとらえよう

  •  先日、ある案件についての技術調査をしていたところ、「アプリケーションドメイン」にたどり着きました。 アプリケーションドメインとはなんぞや、から始まり、MSDN はもちろん、いろいろな方の解説を拝見して、 自分なりに理解できた(ような気がする)ので、以下にまとめたいと思います。

スポンサーリンク


1 exe ファイル、1プロセス。

  •  .NET Framework を使ったアプリケーションでは、1個の exe ファイルを1プロセスと呼ぶことがあります。 例えば、Process.Start メソッド経由で別プログラムを実行することができますが、このクラス名がプロセスですよね。 また、exe ファイルを実行中に、同じ exe ファイルを起動させることで、2つ、3つと起動させて、多重起動することもできます。 ただし、多重起動したとしても、各プログラムは連携して動作するのではなく、それぞれ独立して動作します。 メモリも共有しないので、プロセス間でやり取りすることはできません。

  • イメージ イメージ

  •  ※やろうと思えば、多重起動の禁止も、プロセス間通信の実現も、専用の命令を書くことで可能なのですが、 デフォルトではできない仕様なので、基本設計としてこうなっているんですよ~。ということを言いたいです。

  •  それでは、1つの exe ファイルについて、より詳しく見ていきます。

  • イメージ
  •  普段、画面に入力欄とかボタンとか配置して、イベント書いてメソッド書いて、プログラムを作成していく過程の中で、 アプリケーションドメインがなんちゃらとか、スレッドがどうのこうのとかなんて、意識することは全然無いと思います。 しかし、実はすでに、アプリケーションドメインにも、スレッドにも関わっています。 プロパティやメソッドが何かしらのクラスに属していないといけないように、私たちが意識せずとも、デフォルトのアプリケーションドメインに属して、 UI スレッドというスレッド上で、プログラムは実行されています。以下は、それを確認するサンプルです。

  • Imports System.Threading
    
    Public Class Form1
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    
            Dim processID = Process.GetCurrentProcess.Id
            Dim domainID = AppDomain.CurrentDomain.Id ' Thread.GetDomainID ' Thread.GetDomain.Id
            Dim threadID = Environment.CurrentManagedThreadId ' Thread.CurrentThread.ManagedThreadId
            Console.WriteLine("process = {0}, domain = {1}, thread = {2}", processID, domainID, threadID)
    
        End Sub
    
    End Class
    
    ' 出力結果
    process = 8516, domain = 1, thread = 9
    
  •  これを見ると、プロセスにも ID があり、アプリケーションドメインにも ID があり、スレッドにも ID があることと、 ID が割り当たっているということは、そこに属している。ということが分かります。 コード中でコメントアウトしている命令は、どれも同じ値でした(・・・だったような気がします)。 私たちが意識しなくても属しているということは、私たち側で何らかの面倒を見てあげる必要は無く(xxx という命令を呼び出さないといけないとかは無く)、 自動的に所属されて、自動的に終わるみたいです。だから、アプリケーションドメインもスレッドも、知らなくても問題が起こりません。


スポンサーリンク


別スレッド。

  •  次に、別スレッドについてです。UI スレッドとは違う別のスレッドなので、ここでは別スレッドと言います。 昨今では、PC 以外にスマートフォンやタブレットなどのデバイスの多様化に従い、マルチスレッドや非同期処理が大活躍しているみたいですね。 これらのプログラミングをする際、必ず出てくるのが別スレッドです。フォアグラウンドスレッドとかバックグラウンドスレッドとか。 UI スレッドが動作している裏で、別の処理を動かすことができるので、 画面が応答なしにならずに重たい処理をこなすことができる、うまく使うことで速度改善することができる、というような、1つ上のプログラムにレベルアップすることができます。 ただし、よく分かっていないと扱いが難しいんですよね。

  • イメージ

  •  別スレッドは、別スレッドを作成することで使うことができます。 当たり前でしょ何言ってんの?と突っ込まれそうですが、意図的に、別途作って使うもの。ということを言いたいです。

  •  UI スレッドは、画面上のコントロールへの直接アクセスが可能です。 別スレッドは、画面上のコントロールへの直接アクセスはできません(直接アクセスする命令は書けるしビルドも通るが、実行時に InvalidOperationException が出る)。 なので、Invoke メソッド経由で間接的にアクセスします。Invoke メソッドは、UI スレッド上でコントロールにアクセスしてもらうように、調整依頼の仕事をします。 以下のサンプルは、アクセス時における違いを確認するものです。

  • Public Class Form1
    
        ' 画面上に、ボタンと BackgroundWorker を配置しています。
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    
            Me.Button1.Text = "hello"
            Me.BackgroundWorker1.RunWorkerAsync()
    
        End Sub
    
        Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    
            Try
    
                Me.Button1.Text = "world"
                'Me.Button1.Invoke(Sub() Me.Button1.Text = "world")
    
            Catch ex As Exception
                Console.WriteLine(ex.ToString())
            End Try
    
        End Sub
    
    End Class
    
    ' 別スレッド上から、コントロールを直接アクセスすると・・・
    System.InvalidOperationException: 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Button1' がアクセスされました。
       場所 System.Windows.Forms.Control.get_Handle()
       場所 System.Windows.Forms.Control.set_WindowText(String value)
       場所 System.Windows.Forms.Control.set_Text(String value)
       場所 System.Windows.Forms.ButtonBase.set_Text(String value)
    


  •  フォアグラウンドスレッドとバックグラウンドスレッドの違いは、プログラム終了時の動作が違います(画面右上の×ボタンを押した等)。 フォアグラウンドスレッドがまだ実行中の場合、実行中の、全てのフォアグラウンドスレッド処理が終わるまで、終了せずに待機します。 バックグラウンドスレッドがまだ実行中の場合、実行中の、全てのバックグラウンドスレッド処理を強制的に終わらせて、プログラムを終了します。 以下のサンプルは、この動作の違いを比較したものです。

  • Imports System.Threading
    
    Public Class Form1
    
        ' あらかじめ、画面上にボタンを2つ配置しています。
        ' ボタンを押したら、3秒以内に赤い×ボタンを押す等して、プログラムを終了させます。
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    
            Dim t As New Thread(New ThreadStart(Sub()
                                                    Thread.Sleep(3000)
                                                    Console.WriteLine("3 秒たった!")  ' MsgBox("3 秒たった!")
                                                End Sub))
    
            t.IsBackground = False
            t.Start()
    
        End Sub
    
        Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    
            Dim t As New Thread(New ThreadStart(Sub()
                                                    Thread.Sleep(3000)
                                                    Console.WriteLine("3 秒たった!")  ' MsgBox("3 秒たった!")
                                                End Sub))
    
            t.IsBackground = True
            t.Start()
    
        End Sub
    End Class
    
    ' 出力結果
    Button1 ボタンを押下時のみ、3秒後に「3 秒たった!」と表示
    
  •  このサンプルでは、ボタンを押した後、3秒待機して、メッセージ表示します。 確認内容は、ボタンを押した直後に、赤い×ボタンを押して、プログラムを終了させようとした時の、2つのスレッドの動き方についてです。 その結果は、前述した通りになりました。


アプリケーションドメインの使いどころ。

  •  クライアントアプリケーション開発で、別のアプリケーションドメインが必要になる案件というのは、 外部プログラムの動的読込と実行、動的開放、そして、解放後の外部プログラムを更新したい、変更したい等、解放後の外部プログラムへのアクセス、というものがあります(一例)。 具体的には、Windows サービスや常駐プログラムのように、常時稼働しているタイプのプログラムで、停止や終了をせずに、参照しているプログラムを入れ替えたい、という要望です。 (後から追記、通常のクライアントアプリケーションでもプラグイン機能を持ったものであれば、機能追加や機能の取り外しなどで必要になります)。

  •  通常、いったん、外部プログラムを読み込むと、読み込んだ側のプログラムが終了しない限り、ずっと外部プログラムを掴みっぱなしなので離してくれません (動的開放の問題。本当は、Assembly.LoadFrom(xxx.exe) した後で、Assembly.Close メソッドとか、Assembly.Release メソッドとかあればいいのですが)。 この場合、読み込んだ側のプログラムを終了させることで、外部プログラムが解放されます。

  •  このような仕様に合わせる対策として、メインプログラムとは別に、仲介プログラムを作成してやり取りする、という対処方法があります。 仲介プログラムは、外部プログラムを動的読込して実行、そして終了する。というシンプルなプログラムです。 (機能仕様についても一例)。必要であれば終了前に、関連データを出力しておく。ということもできそうです。

  • イメージ

  •  この対処方法のデメリットは、新たに exe ファイルを用意しなければならない点です。 機能分割という視点で見るとメリットではあるのですが、メンテナンスが増えることになります(微々たるものかもしれないが)。

  •  別解として、exe ファイルを用意しなくてもできる対処方法があります。 それは、現在所属しているアプリケーションドメインとは別に、新しくアプリケーションドメインを作成して、その中で動的読込して実行させる、というものです。 動的実行後、作成したアプリケーションドメインを削除することで、外部プログラムを開放することができます。

  •  ちなみに、天丼すると、別アプリケーションドメインは、別アプリケーションドメインを作成することで使うことができます。 当たり前でしょ何言ってんの?と突っ込まれそうですが、意図的に、別途作って使うもの。ということを言いたいです。

  •  各スレッドは、アプリケーションドメイン間を行き来することができるので、先程の、仲介 exe 用意案と同じことができるようになります。 ただし、操作できるのは、アプリケーションドメイン間を行き来するための、特殊な訓練を受講した人だけです( MarshalByRefObject を継承させたクラスだけ)。

  • イメージ

  •  ここでは、上記問題に対する解決案のプログラムではなく、プロセス、通常のアプリケーションドメイン、各スレッド、 そして、別に作成したアプリケーションドメインの関係性について、サンプルで見てみます。

  • 画面デザイン
  • ここでは見栄を張って綺麗に作りましたが、本当はボタンだけ適当に配置して、Text プロパティもデフォルトのままでいいんです。 ってか、画面タイトルがデフォルトのままだった・・・。いいや。
  • イメージ

    Imports System.Threading
    Imports System.Reflection
    Imports System.Runtime.Remoting
    
    ' まずは、テストデータとなるクラスを準備します。
    ' MarshalByRefObject を継承させているので、アプリケーションドメイン間を行き来しながら仕事することができます。
    Class UtilityData
        Inherits MarshalByRefObject
    
        Public Sub NormalCall()
    
            Dim processID = Process.GetCurrentProcess.Id
            Dim domainID = AppDomain.CurrentDomain.Id ' Thread.GetDomainID ' Thread.GetDomain.Id
            Dim threadID = Environment.CurrentManagedThreadId ' Thread.CurrentThread.ManagedThreadId
            Dim isBackground = Thread.CurrentThread.IsBackground
            Console.WriteLine("process = {0}, domain = {1}, thread = {2}, IsBackground = {3}", processID, domainID, threadID, isBackground)
    
        End Sub
    
        Public Sub ForegroundThreadCall()
    
            Dim t As New Thread(New ThreadStart(Sub()
                                                    Dim processID = Process.GetCurrentProcess.Id
                                                    Dim domainID = AppDomain.CurrentDomain.Id ' Thread.GetDomainID ' Thread.GetDomain.Id
                                                    Dim threadID = Environment.CurrentManagedThreadId ' Thread.CurrentThread.ManagedThreadId
                                                    Dim isBackground = Thread.CurrentThread.IsBackground
                                                    Console.WriteLine("process = {0}, domain = {1}, thread = {2}, IsBackground = {3}", processID, domainID, threadID, isBackground)
                                                End Sub))
            t.IsBackground = False
            t.Start()
    
        End Sub
    
        Public Sub BackgroundThreadCall()
    
            Dim t As New Thread(New ThreadStart(Sub()
                                                    Dim processID = Process.GetCurrentProcess.Id
                                                    Dim domainID = AppDomain.CurrentDomain.Id ' Thread.GetDomainID ' Thread.GetDomain.Id
                                                    Dim threadID = Environment.CurrentManagedThreadId ' Thread.CurrentThread.ManagedThreadId
                                                    Dim isBackground = Thread.CurrentThread.IsBackground
                                                    Console.WriteLine("process = {0}, domain = {1}, thread = {2}, IsBackground = {3}", processID, domainID, threadID, isBackground)
                                                End Sub))
            t.IsBackground = True
            t.Start()
    
        End Sub
    
        Public Sub AsyncCall()
    
            Task.Run(Sub()
                         Dim processID = Process.GetCurrentProcess.Id
                         Dim domainID = AppDomain.CurrentDomain.Id ' Thread.GetDomainID ' Thread.GetDomain.Id
                         Dim threadID = Environment.CurrentManagedThreadId ' Thread.CurrentThread.ManagedThreadId
                         Dim isBackground = Thread.CurrentThread.IsBackground
                         Console.WriteLine("process = {0}, domain = {1}, thread = {2}, IsBackground = {3}", processID, domainID, threadID, isBackground)
                     End Sub)
    
        End Sub
    
    End Class
    
    Imports System.Threading
    Imports System.Reflection
    Imports System.Runtime.Remoting
    
    ' 確認用のプログラムです。
    Public Class Form1
    
    #Region "デフォルトのアプリケーションドメイン"
    
        Private Sub btnUIThread_Click(sender As Object, e As EventArgs) Handles btnUIThread.Click
    
            Dim util As New UtilityData
            util.NormalCall()
    
        End Sub
    
        Private Sub btnForegroundThread_Click(sender As Object, e As EventArgs) Handles btnForegroundThread.Click
            
            Dim util As New UtilityData
            util.ForegroundThreadCall()
    
        End Sub
    
        Private Sub btnBackgroundThread_Click(sender As Object, e As EventArgs) Handles btnBackgroundThread.Click
            
            Dim util As New UtilityData
            util.BackgroundThreadCall()
    
        End Sub
    
        Private Sub btnTaskAsync_Click(sender As Object, e As EventArgs) Handles btnTaskAsync.Click
            
            Dim util As New UtilityData
            util.AsyncCall()
    
        End Sub
    
    #End Region
    
    #Region "別作成したアプリケーションドメイン"
    
        Private Sub btnUIThread2_Click(sender As Object, e As EventArgs) Handles btnUIThread2.Click
    
            Dim nd = AppDomain.CreateDomain("NewDomain1")
            Dim dt1 = nd.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, GetType(UtilityData).FullName)
            Dim dt2 = CType(dt1, UtilityData)
            dt2.NormalCall()
            Thread.Sleep(100) ' ちょっとだけ待って、表示された後でアンロードされるように微調整
            'Console.WriteLine("透過プロキシになっているか = " & RemotingServices.IsTransparentProxy(dt2))
            AppDomain.Unload(nd)
    
        End Sub
    
        Private Sub btnForegroundThread2_Click(sender As Object, e As EventArgs) Handles btnForegroundThread2.Click
            
            Dim nd = AppDomain.CreateDomain("NewDomain2")
            Dim dt1 = nd.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, GetType(UtilityData).FullName)
            Dim dt2 = CType(dt1, UtilityData)
            dt2.ForegroundThreadCall()
            Thread.Sleep(100) ' ちょっとだけ待って、表示された後でアンロードされるように微調整
            'Console.WriteLine("透過プロキシになっているか = " & RemotingServices.IsTransparentProxy(dt2))
            AppDomain.Unload(nd)
    
        End Sub
    
        Private Sub btnBackgroundThread2_Click(sender As Object, e As EventArgs) Handles btnBackgroundThread2.Click
            
            Dim nd = AppDomain.CreateDomain("NewDomain3")
            Dim dt1 = nd.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, GetType(UtilityData).FullName)
            Dim dt2 = CType(dt1, UtilityData)
            dt2.BackgroundThreadCall()
            Thread.Sleep(100) ' ちょっとだけ待って、表示された後でアンロードされるように微調整
            'Console.WriteLine("透過プロキシになっているか = " & RemotingServices.IsTransparentProxy(dt2))
            AppDomain.Unload(nd)
    
        End Sub
    
        Private Sub btnTaskAsync2_Click(sender As Object, e As EventArgs) Handles btnTaskAsync2.Click
            
            Dim nd = AppDomain.CreateDomain("NewDomain4")
            Dim dt1 = nd.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, GetType(UtilityData).FullName)
            Dim dt2 = CType(dt1, UtilityData)
            dt2.AsyncCall()
            Thread.Sleep(100) ' ちょっとだけ待って、表示された後でアンロードされるように微調整
            'Console.WriteLine("透過プロキシになっているか = " & RemotingServices.IsTransparentProxy(dt2))
            AppDomain.Unload(nd)
    
        End Sub
    
    #End Region
    
    End Class
    
    ' 出力結果
    process = 8812, domain = 1, thread = 9, IsBackground = False
    process = 8812, domain = 1, thread = 10, IsBackground = False
    process = 8812, domain = 1, thread = 11, IsBackground = True
    process = 8812, domain = 1, thread = 12, IsBackground = True
    process = 8812, domain = 2, thread = 9, IsBackground = False
    process = 8812, domain = 3, thread = 11, IsBackground = False
    process = 8812, domain = 4, thread = 10, IsBackground = True
    process = 8812, domain = 5, thread = 12, IsBackground = True
    
    
  •  先程のサンプルの結果とプロセス ID が違っていますが、実行するたびに割り当たる数字は変わってきます。 この結果は、画面左側のグループボックスより上から、「UI スレッド」ボタン、・・・、「タスクの非同期」ボタン、 そして、画面右側のグループボックスより上から、「UI スレッド」ボタン、・・・、「タスクの非同期」ボタン、と押していったものです。 もう作ってしまったので直しませんが、別で作成したアプリケーションドメインは、クラスメンバーとかにして、1つだけ作成した方が良かったですね。 全部 domain = 2 なら、さらに分かりやすかったと思います(反省。するけど直さないごめんなさい_ _;)。

  •  アプリケーションドメインだけだと何となく分かった感じになりますが、関連するものも含めて全体で見てみると、・・・やはり、何となく分かった感じでしょうか。 読んでもよく分からない方は、実際に手を動かしてみる、それも、コピペじゃなくて1文字1文字手打ちすることで、分かるかもしれないです。

  •  ところで、UI スレッドって、フォアグラウンドスレッドだったんですね。だから分別すると、処理が終わるまで待機する方です。 あと非同期処理って、やっぱり別スレッドで、さらにバックグラウンドスレッドだったんですね。だから分別すると、処理の途中でも強制終了させられる可能性があります。 と言っても、実際の業務プログラムでは、面倒見なきゃいけないと思うので、フォアグラウンドスレッドにしろバックグラウンドスレッドにしろ、 どちらにしても、処理が終了するまで待ってあげて、UI スレッドに合流するような実装になるのではないでしょうか。 途中終了するにしても同様です。


参考にさせていただいた記事


スポンサーリンク