実行コンテキスト
久々に、そのうち http://ufcpp.net/study/csharp/ に載せる前提の下書き的なブログ。
概要
.NET Frameworkのスレッドは、実行コンテキスト(execution context: 実行の文脈)というものを持っています。
「文脈」という言葉の意味するところは、「意識することなく皆が共有している情報」ということです。実行コンテキストの場合は、以下のような情報を、スレッドを超えて共有します。
- セキュリティ コンテキスト: どういう権限でそのスレッドが動いているかを伝搬して、適切なセキュリティを保つ
- 論理呼び出しコンテキスト: (実際の呼び出しスタック上の上下関係でな
- く、)論理的な呼び出し関係での情報共有
情報の共有範囲
実行コンテキストを理解するためには、まず、どういう範囲で情報を共有できればいいのかという話をしましょう。
静的フィールド
オブジェクトをまたいだ情報の共有というと、まず思い浮かぶのは静的フィールドでしょう。
class Program
{
static int StaticField;
static void Main()
{
StaticField = 123;
var x = StaticField;
}
}
しかし、静的フィールドは、共有範囲としては広すぎで、使えない場面もあります。
例えば、ウェブ サーバーを考えてみましょう。
1つのプロセス内で複数のHTTPリクエストをさばく※とき、リクエストをまたいで情報共有するのはいろいろな危険があります。クライアントごとに与えられる権限が異なるわけで、同じ文脈を持ってリクエストを処理するとセキュリティ的なリスクになりかねません。
※ 実際、IISは1プロセス内で複数のリクエストを処理します。
スレッドローカル
共有範囲を絞る方法の1つに、スレッド ローカル変数というものがあります。.NET Frameworkの場合、ThreadLocal<T>クラス(System.Threading名前空間)を使います。
using System.Threading;
class Program
{
static ThreadLocal<int> ThreadLocal = new ThreadLocal<int>();
static void Main()
{
ThreadLocal.Value = 123;
var x = ThreadLocal.Value;
}
}
これを使うと、1つのスレッド内からだけしか読み書きできない変数を作れます。この例の場合、ThreadLocal は静的フィールドですが、そのValueプロパティに格納される値はスレッドごとに別になります。例えば、以下のようになります。
ThreadLocal.Value = 123;
Task.WhenAll(
Task.Run(() => ThreadLocal.Value = 1), // 別スレッドで上書きしても
Task.Run(() => ThreadLocal.Value = 2),
Task.Run(() => ThreadLocal.Value = 3))
.Wait();
var x = ThreadLocal.Value; // 変化しない
Console.WriteLine(x); // 123 のまま
これで、先ほどの要件(ウェブ サーバー)でいうと、1リクエスト1スレッドで処理する分には問題がなくなりそうです。しかし通常、1リクエストの処理中に、複数のスレッドを使うことがあります。というか、async/awaitがある今、普通に複数のスレッドを使います。
また、Taskクラス(System.Threading.Tasks名前空間)はスレッド プール(同じスレッドを何度も使いまわして効率よく非同期処理する仕組み)を使うわけですが、この場合、リクエストを超えて同じスレッドが再利用される場合もあって、要件に合いません。
この要件では、「スレッドは超えれるけども、リクエストは超えれない」というような共有範囲が必要になります。
実行コンテキスト
ということで、適切な情報共有のためにあるのが実行コンテキストです。
つまり、.NET Frameworkのスレッドには以下のような機能があって、これを実行コンテキストと呼びます。
- 基本的にはスレッドをまたいで自動的に情報を伝搬させる
- 明示的に伝搬を抑止することもできる(上記の例の場合、リクエスト単位で抑止)
実行コンテキストの実体はExecutionContextクラス(System.Threading名前空間)ですが、このクラスが直接使われることはあまりありません。というより、主だった機能がinternalになっていて、他のクラスを介して触れることになります。例えば以下のようなものです。
-
セキュリティ コンテキスト
- SecurityContextクラス(System.Security名前空間)
- サンドボックス内で動いているか全信頼で動いているかといった情報や、Windows認証に関する情報を持っている
- SecurityContextクラス(System.Security名前空間)
-
論理呼び出しコンテキスト
- CallContextクラス(System.Runtime.Remoting.Messaging名前空間)
- (実行コンテキスト内で)スレッドをまたいで任意の情報を共有するためのスロット(データ格納場所)
- CallContextクラス(System.Runtime.Remoting.Messaging名前空間)
例えば、以下のようなコードで、実行コンテキスト内での情報共有ができます(先ほどのウェブ サーバーの要件はこれで満たせます)。
using System.Runtime.Remoting.Messaging;
class Program
{
static void Main()
{
CallContext.LogicalSetData("Value", 123);
var x = CallContext.LogicalGetData("Value");
}
}
ちなみに、「論理呼び出し」というのは、以下のように、Task.Runなどを介した呼び出し方も含めた呼び出し関係のことです。
static void Main()
{
Sub(); // 直接の呼び出し関係
Task.Run(() => Sub()); // 論理的な呼び出し関係
// Sub を直接呼んでいるのはラムダ式だし、
// そのラムダ式も、元をたどっていくと TaskScheduler が呼んでる。
// でも、論理的には Main から Sub を呼んでいるようなもの
}
static void Sub() { }
その他、補足や、回を改めて
そもそもこういう文脈を持つのは割と最終手段。あんまり文脈には頼らない方がいい。「staticはダメ」って言われるのもそのため(正確には書き換え可能な静的フィールドが危険なのであって、constとか静的メソッドは平気)。
別途、同期コンテキスト(synchronization context)の話もする。
同期コンテキストは、基本的に実行コンテキストと一緒に伝搬すべきではない。実際、WPFとかWinFormsのUI同期コンテキストは伝搬しない。ASP.NETの同期コンテキストだけは実行コンテキストで伝搬しちゃうんだけども、「余計なお世話」感はんぱない。
ちなみに、ASP.NETの同期コンテキストは、
- どこのスレッドで実行するかはわからないけども、1度に1つずつ、enqueueされた順序通りに実行することを保証
- リクエスト処理が終わった後にはもうPostできない
とかいう処理をしてる。これもまた「余計なお世話」。こんな同期コンテキスト正直要らない。
OWINを使うと、System.Webをバイパスしてしまえるので、このASP.NET同期コンテキストも消せる(IIS直接使う(Helios)とか、Self-Hostとかの場合、SynchronizationContext.Currentはnull)。
コメントを残す