翻訳元:ハイパーリンクのログインが見えます。
概要: たとえ自分が何をしているか分かっていると思っても、ASP.Net アプリケーションでThreadStaticメンバー、CallContext、Thread Localストアに何も保存するのは安全ではありません。もし値が事前に設定されている場合は。 Page_Load(例:IHttpModuleやページコンストラクタ)に、アクセス中またはその後に。
【更新:2008年8月 この記事にリンクを続ける人がかなりいるため、このスレッドスワップの挙動はページのライフサイクルの非常に特定の時点で発生しており、気分のタイミングで起こるわけではないことを明確にする必要があると感じます。】 ジェフ・ニューサムの言葉を引用した後の私の表現は残念です。 それ以外では、HttpContextの適切な取り扱いに関する設計議論でこの記事が何度も参照されているのを見て、とても嬉しく(そして光栄に思います)。 役に立つと感じる人がいるのは嬉しいです。 ]
ASP.Net でユーザー固有のシングルトンをどのように実装するかについて多くの混乱があります。つまり、グローバルデータは1つのユーザーやリクエストに対してグローバルに割り当てられているということです。 これは珍しい要件ではありません。トランザクションデータ、セキュリティコンテキスト、その他の「グローバル」データを一箇所に公開し、すべてのメソッド呼び出しにプッシュするのではなく、ロームデータにより明確で読みやすい実装を可能にします。 しかし、注意しないと、ここで自分の首(または頭)を撃つ絶好のチャンスになります。 何が起きているのか分かっているつもりでしたが、実際はそうではありませんでした。
望ましい選択肢はシングルトンですHttpContext.Current.Itemsに保存され、シンプルかつ安全ですただし、問題のシングルトンを ASP.Net アプリケーションでの使用と関連付けています。 もしビジネスオブジェクトのシングルトンが失敗した場合、これは理想的ではありません。 たとえプロパティアクセスをif文でラップしてもです
Summary: たとえ自分が何をしているか分かっていると思っていても、もし可能ならば、ASP.Net アプリケーション内のThreadStaticメンバー、CallContext、またはThreadローカルストレージに何かを保存するのは安全ではありません 値はPage_Loadの前に設定される場合(例:IHttpModuleやページコンストラクタ)が、中またはその後にアクセスされる場合もあります。
【更新:2008年8月】この投稿にリンクを続けている人がかなり多いことを考慮し、このスレッドスワッピングの動作はページの非常に特定のポイントで起こっていることを明確にする必要を感じています ライフサイクルであり、気分のどのタイミングで行くかではありません。 ジェフ・ニューソンの引用後の私の言い回しは残念でした。 それはさておき、この投稿がHttpContextを適切に扱う設計議論の中で何度も引用されているのを見て、非常に嬉しく(そして光栄に感じています)と共感しています。 役に立った人もいただけて嬉しいです。]
ユーザー固有のシングルトンを ASP.Net で実装する方法、つまりグローバルなデータを一つのユーザーやリクエストに限定する方法については多くの混乱があります。 これは珍しい要件ではなく、トランザクションやセキュリティコンテキスト、その他の「グローバル」データを一箇所に公開し、トランプデータのようにすべてのメソッド呼び出しに押し込むのではなく、 よりクリーンで(読みやすい)実装。 しかし、注意しないと自分の足(または頭)を撃つ絶好の場所です。 何が起きているのか分かっているつもりでしたが、実際はそうではありませんでした。
望ましい方法は、シングルトンをHttpContext.Current.Itemsに保存するというシンプルで安全ですが、問題のシングルトンを ASP.Net アプリケーション内で使用することに結びつきます。 もし独身者があなたのビジネスオブジェクトにいるなら、これは理想的ではありません。 たとえプロパティアクセスをif文でラップしても
... それでもそのアセンブリからSystem.Webを参照しなければならず、そうすると「webby」オブジェクトが誤った場所に集まりやすいです。
代替手段としては、[ThreadStatic]の静的メンバー、Threadローカルストレージ(ほぼ同じもの)、またはCallContextを使う方法があります。
[ThreadStatic]の問題点はよく記録されていますが、まとめると: フィールドイニタライザーは最初のスレッドでのみ発動します ThreadStaticのデータは明示的にクリーンアップする必要があります(例:EndRequestで)。スレッドは到達可能ですが、ThreadStaticデータはGC化されないため、リソースをリークする可能性があります。 ThreadStaticデータはリクエスト内でしか有効ではありません。なぜなら、次のリクエストが別のスレッドから入ってきて、他の誰かのデータを取得する可能性があるからです。 スコット・ハンセルマンは正しく指摘しています。ThreadStaticは ASP.Net と相性が良くないが、その理由を完全には説明していない。
CallContextのストレージはこれらの問題の一部を緩和します。なぜなら、リクエストの最後にコンテキストが終了し、最終的にGCが発生します(ただし、GCが起こるまでリソースをリークすることは可能です) 使い捨てを保管しているのです。 さらに、CallContextはHttpContextを保存する方法なので、問題ないはずですよね? いずれにせよ、私が思っていたように、依頼の最後に片付けていれば何も問題ないと思うでしょう: 「リクエストの最初にThreadStatic変数を初期化し、リクエストの最後に参照されたオブジェクトを正しく扱えば、何も悪いことが起きないと主張するリスクを負います
「さて、間違っているかもしれません。 CLRはスレッドの途中でホスティングを停止し、そのスタックをどこかでシリアライズし、新しいスタックを与えて実行を開始させるかもしれません。 私はこれを深く疑っています。 ハイパースレッディングが難しくなる可能性はあるかもしれませんが、私もそうは思いません。 ”
ジェフ・ニューサム
「もしリクエストの最初にThreadStatic変数を初期化し、リクエストの最後に参照対象のオブジェクトを適切に処分した場合、私は何もないと主張します 悪いことが起こる。 同じAppDomain内のコンテキスト間でもあなたもクールです
「まあ、間違っているかもしれない。 clrは管理スレッドを途中で停止し、そのスタックをどこかでシリアライズし、新しいスタックを与えて実行を開始する可能性があります。 私は本当にそうは思いません。 ハイパースレッディングが難しくなる可能性も考えられますが、それも疑わしいと思います。」 ジェフ・ニューサム 追記:これは誤解を招く内容です。 このスレッドの入れ替えはBeginRequestとPage_Loadの間でしか起こり得ないことを後ほど詳しく説明しますが、Jefのリファレンスは非常に強力なイメージを作り出しており、すぐに修正しませんでした。
Update: This was the misleading bit. I do explain further later on that this thread-swap can only happen between the BeginRequest and the Page_Load, but Jef's quote creates a very powerful image I failed to immediately correct. My bad. ある時点で、他のリクエストを処理するI/Oスレッドが多すぎると判断 ASP.NET。 […]リクエストのみを受け付け、実行時間内に内部キューオブジェクトにキュー ASP.NET 入れます。 キューを行った後、I/Oスレッドはワーカースレッドを要求し、その後I/Oスレッドは元のプールに戻ります。 […]したがって ASP.NET その担当者に依頼を任せます。 低負荷時のI/Oスレッドのように、ASP.NET 実行時まで対応します。
ある時点で ASP.NET 他のリクエストを処理するI/Oスレッドが多すぎると判断します。 [...] リクエストを受け取り、ASP.NET ランタイム内にこの内部キューオブジェクトにキューアップします。 その後、それがキューに入った後、I/Oスレッドはワーカースレッドを求め、I/Oスレッドは元のプールに戻されます。 [...] ですので ASP.NET そのワーカースレッドにリクエストを処理させます。 低負荷時のI/Oスレッドと同様に、ASP.NET 実行時間に持ち込みます。 ずっと知っていましたが、とても早い段階で気にしていませんでした。 しかし、私の考えは間違っているようです。 ASP.Net アプリで、ユーザーが別のリンクをクリックした後にリンクをクリックした際に、あるシングルトンにnull reference例外があったことがあります(シングルトンにはThreadStaticではなくCallContextを使いましたが、結局関係ありませんでした)。
スレッドの仕組み ASP.Net について調べたところ、事実を装った相反する意見(リクエストはリクエストではスレッドアジャイルですが、リクエストはライフ期間中にスレッドにアンカーされます)を得たので、遅いページ(一瞬スリープ)と高速ページを持つテストアプリケーションに問題をコピーしました。 遅いページのリンクをクリックし、ページが戻る前に速いページのリンクをクリックします。 その結果(log4netで何が起きているのかのダンプ)には衝撃を受けました。
出力は、2回目のリクエストに対して、HttpModuleパイプラインのBeginRequestイベントとページコンストラクタが1つのスレッドで起動し、別のスレッドでpage_Loadされることを示しています。 2番目のスレッドはすでに1番目のスレッドからHttpContextを移行していますが、CallContextやThreadStaticは移行していません(注:HttpContext自体はCallContextに保存されているため、ASP.Net は明示的にHttpContextを移行しています)。 もう一度言いましょう:
これは最初から知っていましたが、プロセスの早い段階で起きたものだと思い込んでいたので気にしませんでした。 しかし、どうやら私の考えは間違っていたようです。 私たちの ASP.Net アプリで問題が発生しています。ユーザーがリンクをクリックした直後に別のリンクをクリックすると、アプリがシングルトンの一つに対してnull reference例外が発生してしまいます(私は使っています シングルトンの場合はThreadStaticではなくCallContextですが、結局問題ではありませんでした。
ASPが正確にどうなっているのか少し調べました。 Netのスレッドは動作し、事実を装った意見(リクエストはリクエスト内でスレッドアジャイルとリクエストはライフ期間スレッドにピン留めされる)意見が分かれていたので、私は テストアプリケーションで、遅いページ(一瞬スリープ)と速いページで問題が発生しました。 遅いページのリンクをクリックし、ページが戻る前に速いページのリンクをクリックします。 結果(log4netの内容のダンプ)には驚きました。
出力が示すのは、2回目のリクエストではHttpModuleパイプラインのBeginRequestイベントとページコンストラクタは1つのスレッドで起動しますが、Page_Loadは別のスレッドで発生します。 2番目のスレッドではHttpContextは1つ目から移行されましたが、CallContextやThreadStaticは移行していません(注:HttpContext自体がCallContextに保存されているため、ASP.Net は HttpContextを明示的に移行しています)。 もう一度はっきり言いましょう:
- スレッドスイッチングはIHttpHandlerが作成された後に行われます
- ページのフィールド初期化器とコンストラクタ実行後
- Global.ASA/IHttpModulesで使用されるBeginRequest、AuthenticateRequest、AquireSessionState型イベントの後、
- 新しいスレッドに移行されるのはHttpContextだけです
スレッドスイッチはIHttpHandlerが作成された後に行われます ページのフィールドイショナイザとコンストラクタ実行後に Global.ASA / IHttpModulesが使用しているBeginRequest、AuthenticateRequest、AquireSessionStateのタイプイベントの後、 新しいスレッドに移行するのはHttpContextだけです これは非常に面倒です。なぜなら、私が見た限りでは、ASP.Net の「ThreadStatic」クラスの挙動に対する唯一の永続化オプションはHttpContextを使うことを意味するからです。 ビジネスオブジェクトについては、if(HttpContext.Current!)を使い続けてください。 =null)とSystem.Web参照(嫌な感じ)を用意するか、静的永続性のためのプロバイダーモデルを考え出す必要があり、それをセットアップしてからこれらのシングルトンにアクセスする必要があります。 吐き気が二重です。
どうかそうではないと言ってください。
付録:全記録:
これは非常に面倒な問題です。なぜなら、私の理解では、ASP.Net における『ThreadStatic』のような挙動に対する永続化の選択肢はHttpContextを使うことだけだからです。 したがって、ビジネスオブジェクトについては、if(HttpContext.Current!=null)とSystem.Web参照(うわい)で縛られるか、あるいは何らかのプロバイダーモデルを考えなければなりません 静的永続性(static persistence)は、これらのシングルトンのいずれかにアクセスする前に設定する必要があります。 二重に嫌だ。
誰かそうじゃないと言ってくれ。
Appendix: That log in full: [3748] 情報 11:10:05,239 ASP。 Global_asax。 Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/SlowPage.aspx [3748] 情報 11:10:05,239 ASP。 Global_asax。 Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=97, calldata= [3748] 情報 11:10:05,249 ASP。 SlowPage_aspx... CTOR() - ThreadID=3748, threadHash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] 情報 11:10:05,349 ASP。 SlowPage_aspx。 Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] 情報 11:10:05,349 ASP。 SlowPage_aspx。 Page_Load() - スローページスリープ...
[2720] 情報 11:10:05,669 ASP。 Global_asax。 Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/FastPage.aspx [2720] 情報 11:10:05,679 ASP。 Global_asax。 Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=1835, calldata= [2720] 情報 11:10:05,679 ASP。 FastPage_aspx... CTOR() - ThreadID=2720, threadhash=(cctor)1835, threadhash(now)=1835, calldata=2720, logicalcalldata=2720, threadstatic=2720
[3748] 情報 11:10:06,350 ASP。 SlowPage_aspx。 Page_Load() - ゆっくりしたページが起きる... [3748] 情報 11:10:06,350 ASP。 SlowPage_aspx。 Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748 [3748] 情報 11:10:06,350 ASP。 Global_asax。 Application_EndRequest() - threadid=3748, threadhash=97, threadhash(now)=97, calldata=3748 [3748] 情報 11:10:06,350 ASP。 Global_asax。 Application_EndRequest() - 終了 /ConcurrentRequestsDemo/SlowPage.aspx
[4748] 情報 11:10:06,791 ASP。 FastPage_aspx。 Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(now)=1703, calldata=, logicalcalldata=, threadstatic= [4748] 情報 11:10:06,791 ASP。 Global_asax。 Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(now)=1703, calldata= [4748] 情報 11:10:06,791 ASP。 Global_asax。 Application_EndRequest() - 終了 /ConcurrentRequestsDemo/FastPage.aspx 重要なのは、FastPageのPage_Loadが作動したときに何が起こるかです。 ThreadIDは4748ですが、ctorのHttpContextに保存されているThreadIDは2720です。 論理スレッドのハッシュコードは1703ですが、ctorに保存しているハッシュコードは1835です。 CallContextに保存したすべてのデータ(ILogicalThreadAffinnativeとマークされたデータも含む)が欠けていますが、HttpContextはまだ残っています。 予想通り、ThreadStaticも消えてしまいました。
重要なのは、FastPageのPage_Loadが発射されたときに何が起こるかです。 ThreadIDは4748ですが、ctorのHttpContextに保存したthreadIDは2720です。 論理スレッドのハッシュコードは1703ですが、ctorに保存したものは1835です。 CallContextに保存したすべてのデータは消えています(ILogicalThreadAffinativeとマークされたデータも含めて)が、HttpContextはまだ残っています。 予想通り、ThreadStaticも消えてしまいました。 (終わり)
|