プロセスレベルのグローバル変数(Singleton, Shared, Module)と、スレッドレベルのグローバル変数(ThreadStatic=スレッドローカルストレージ)

サンプルコードGlobalInformation.zip 直

.Netでのグローバル変数の持ち方について考えてみます。

グローバル変数は、変数の生存期間(ライフタイム)と適用範囲(スコープ)を最小にするという原則に従って極力使わないようにするべきものですが、逆に言えば生存期間がアプリケーション開始直後から終了まで、適用範囲がアプリケーション全体に渡る変数はグローバル変数で保持するべきと言えます。設定ファイルの内容だとか、アプリ全体の状態などが該当すると思います。

グローバル変数というと一般的にはプロセスレベルですが、スレッドレベルでもグローバル変数を保持したい場合があります。よくあるのは、多数のクライアントからリクエストを受け付けるサーバアプリケーションでクライアントのリクエストに含まれるユーザIDやIPアドレスなどがあります。.Net RemotingやWCFで有効なプログラミングテクニックです。引数として各処理に渡しても問題ありませんが、リクエストを処理する全関数に引数として渡すのは煩雑なのでグローバル変数を使います。

スレッドレベルのグローバル変数を使うにはスレッドローカルストレージ(TLS)を使います。TLSは.NETだけでなくマルチスレッドを扱うことの出来る言語では用意されている一般的なものです。.NETでは変数定義にThreadStatic属性を付けるだけで簡単に変数をTLSに設定することができます。TLSに設定されたデータの読み書きは、そのスレッドだけに限定されるのでマルチスレッド特有の排他制御の問題は発生しません。プロセスレベルのグローバル変数をThreadIDで管理するよりも安全で効率的なのでスレッドレベルのグローバル変数を持つ場合は必ずTLSを使用します。


Public Class ThreadInformation

_
Public Shared Name As String

End Class

ThreadAttribute属性を付与するプロパティは、初期値を設定してはいけません。(初期値を設定して初期化しても問題ありませんが、意図した通りに初期化してくれるのはメインスレッドのみです。以降のスレッドでは0や空文字、Nothingに初期化されます。)以下は、MSDNの引用です。

ThreadStaticAttribute でマークしたフィールドの初期値を指定しないでください。このような初期化は、クラスのコンストラクタの実行時に一度だけ行われるもので、関係するスレッドは 1 つだけです。初期値を指定しなければ、フィールドが、値型の場合はその既定値に、参照型の場合は nullNothingnullptrnull 参照 (Visual Basic では Nothing) に初期化されることを前提にできます。

検証コード


Public Class Form1

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

Debug.Print(ThreadInformation.Name)
ThreadInformation.Name = "Hello2"
Debug.Print(ThreadInformation.Name)

Dim t As New Threading.Thread(AddressOf ThreadStart)
t.Start()
t.Join()

Debug.Print(ThreadInformation.Name)

End Sub

Public Sub ThreadStart(ByVal param As Object)

Debug.Print(ThreadInformation.Name)

ThreadInformation.Name = "Hello3"
Debug.Print(ThreadInformation.Name)

GlobalInformation .Name = "tekk"

End Sub

End Class

Public Module GlobalInformation

Public Name As String = String.Empty

End Module

Public Class ThreadInformation

_
Public Shared Name As String = "Initialize Main Thread Only"

End Class

イミディエイトウインドウに以下のように出力されます。

Initialize Main Thread Only
Hello2

Hello3
Hello2

次にグローバル変数の定義の仕方を見ていきます。シングルトンデザインパターン、Sharedキーワード、Moduleの3種類で見ていきます。


Public Class GlobalInformation

Private Sub New()
' nop
End Sub

Private Shared _instance As New GlobalInformation

Public Shared Function Instance() As GlobalInformation
Return _instance
End Function

Public Name As String = String.Empty

End Class

コンストラクタをPrivateとしているので、このクラスはNewできません。

シングルトンデザインパターンだと使用時は以下のようになります。Instanceと書かなければいけない所が少し面倒です。


GlobalInformation.Instance.Name = "tekk"

  • Sharedキーワードによるグローバル情報


Public Class GlobalInformation

Public Shared Name As String = String.Empty

End Class

使用時は以下のようになります。かなりスッキリします。但し、使うメンバのすべてにSharedキーワードを付ける必要があります。グローバル情報を定義する際が少し面倒ですが、使用時の記載が簡略化できます。


GlobalInformation.Name = "tekk"

  • Moduleによるグローバル情報

VB.NETでしか使えませんが、Moduleを使用すると自動的にSingletonなクラスとなります。インスタンスが存在するSingletonクラスなのでSharedを付ける必要はありません。


Public Module GlobalInformation

Public Name As String = String.Empty

End Module

使用時は以下となります。


GlobalInformation.Name = "tekk"