更新履歴(初版のみ・2026年09月15日公開)
- 初版公開
計測ボタンを押すと、メーカー製COMのReadが呼ばれます。値は返ってくるものの、ときどき数秒かかり、その間はウィンドウを動かせません。そこで呼び出しをTask.Runへ移したのに、画面はやはり固まります。この記事はその状態から始めます。
変えるのはCOMを呼ぶ場所ではなく、COMを作る場所です。COMのためだけのSTAスレッドを一本用意し、生成から解放までをそのスレッドの中で済ませます。画面はそのスレッドに「値を読んでほしい」と頼み、結果の値だけを受け取ります。COMの参照はスレッドの外へ出しません。
以下ではTask.Runで直らない理由を確かめてから、この専用スレッドの骨格を作ります。続いて、骨格だけでは起きる三つの問題、つまり処理中の要求の割り込み、イベント通知で互いを待って止まること、終了の順番を、配布サンプルのコードで一つずつ解決します。
対象は専用STA内で生成・利用できる非表示のCOMコンポーネントです。画面に貼り付けたActiveXコントロールや、特定のメインSTAを要求する製品を、そのまま移す例ではありません。サンプルはWindows・x64・.NET 8以降のC#で、メッセージ処理にWPFのDispatcherを使います。.NET Frameworkへそのままコピーする前提ではありません。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全5件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
画面が固まる理由
画面が固まるのは、COMがUIスレッドで動いているからです。Task.Runへ移しても直らない理由は、COMオブジェクトが属するアパートメントにあります。
COM(Component Object Model)はソフトウェア部品がインターフェースを通じて連携するためのバイナリ標準です。プログラミング言語そのものではなく、部品の操作を呼び出すための共通の規則を定めます。[com]
STA(Single-Threaded Apartment)は一本のスレッドに属するCOMのアパートメントです。そのSTAに属するオブジェクトへの呼び出しは、所有するスレッドで実行されます。一つのSTAに複数のオブジェクトを置くこともできます。[sta]
UIスレッドで作ったSTAのオブジェクトは、別のスレッドから呼んでも、そのスレッドへは移りません。境界を越える呼び出しはマーシャリングされ、元のSTA、つまりUIスレッドで実行される場合があります。マーシャリングはアパートメントなどの境界を越えてインターフェースを利用できるようにするCOMの処理です。境界の向こうから呼び出せることと、オブジェクトの所属先が移ることは同じではありません。[sta]
flowchart TB
accTitle: UIで生成したSTAのCOMはUIスレッドで実行される
accDescr: UIスレッドで生成したCOMへの呼び出しをTask.Runのスレッドから行っても、UIのSTAへマーシャリングされてUIスレッドで実行されるため、処理中は画面が応答しません。
A["UIスレッドでCOMを生成"] --> B["Task.Runのスレッドから呼ぶ"]
B --> C["UIのSTAへマーシャリング"]
C --> D["UIスレッドで実行"]
D --> E["処理中は画面が応答しない"]
図1: 呼び出す側を別スレッドへ移しても、COMが属するSTAは変わりません。
このときTask.RunのスレッドはUIスレッドに仕事を頼んで待っているだけです。COMの処理が数秒かかれば、その間UIスレッドは画面を更新できません。直すべきは呼び出す場所ではなく、COMを生成する場所です。
COMの居場所を専用のSTAスレッドに移す
COMのためのSTAスレッドを一本用意し、次の三つを約束事にします。この記事では、この形の実装を専用STA COMワーカーと呼びます。
- COMの生成・呼び出し・イベント解除・解放を、すべて同じ専用STAで行う。
- 画面へはCOMの参照を渡さず、計測値のような独立した値だけを返す。
- 専用STAは要求を待っている間もメッセージを処理できるようにしておく。
1と3はCOMをSTAとして初期化するスレッドに求められる条件でもあります。各オブジェクトを一つのスレッドから使い、そのスレッドにメッセージループを持たせることです。[init-library]
flowchart TB
accTitle: 画面は値を受け取りCOMは専用STA内に残る
accDescr: UIから要求キューへ仕事を渡し、専用STA内のセッションがCOMを利用します。戻すのは参照ではなく独立した値です。
UI["UI:ReadAsyncをawait"] --> Q["要求キュー"]
Q --> S["専用STA:セッションとCOM"]
S --> D["結果の値"]
D --> UI
図2: 画面は要求を出して値を受け取るだけで、COMは専用STAの外へ出ません。
1と2を守れば、どこで呼び出し、いつ解放するかをワーカーの中だけで決められます。COMを別アパートメントから使うこと自体が禁止なのではなく、境界を増やさない方針です。[sta] 画面は結果をawaitで受け取り、UIスレッドで同期的に待つことを避けます。[ui]
専用STAワーカーはCOMを速くする仕掛けではなく、使う場所と使い終える手順を限定する実装です。COMの処理にかかる時間は変わりません。変わるのは、その間も画面が応答を続けられることです。
部品の分担
配布サンプルでは画面へ公開する入口、要求を運ぶワーカー、COMを所有するセッション、UIへの通知を分けています。
StaInstrument:画面へReadAsyncとStopAsyncを公開する業務用の入口です。生成用のStartAsyncも持ち、内部でStaWorker<ProbeSession>を起動します。StaWorker<T>:専用STA、Dispatcher、要求キューと終了待ちを管理する内部実装です。ProbeSession:COM参照とイベントの受け手を所有する、製品依存の部分です。実際の製品ではこのクラスをベンダーのCOMに合わせて書きます。UiMailbox:独立した値をUIへPostし、閉じた画面への配信を止める部品です。
画面からの要求はStaInstrumentとStaWorker<T>を通ってProbeSessionのCOMへ届きます。COMイベントの値はUiMailboxを通って画面へ戻ります。
flowchart TB
accTitle: 要求とイベントの値が通る部品
accDescr: 画面の要求はStaInstrumentとStaWorkerを通ってProbeSessionのCOMへ届き、COMイベントの値はUiMailboxから画面へ戻ります。
UI["画面"] --> I["StaInstrument"]
I --> W["StaWorker:専用STA"]
W --> P["ProbeSession:COM参照"]
P -.->|"イベントの値"| M["UiMailbox"]
M -.->|"Post"| UI
図3: 要求は実線の経路でCOMへ届き、イベントの値は点線の経路で画面へ戻ります。
以下のコードは配布ソースから説明に必要な部分を抜き出したものです。断片だけで全体が完成するわけではなく、ビルドできる一式は「サンプルで動作を確かめる」のZIPに含まれます。
画面には業務の操作を見せる
画面に必要なのはCOMオブジェクトではなく、計測値と、処理が終わったという通知です。そこでStaInstrumentは生成用のStartAsyncのほかに、読み取りのReadAsyncと終了のStopAsyncだけを公開します。
// サンプルのStaInstrumentが画面へ見せる操作
public Task<int> ReadAsync(int value)
=> _worker.InvokeAsync(session => session.Read(value));
public Task StopAsync() => _worker.StopAsync();
sessionはCOM参照とイベントの受け手を持つProbeSessionです。画面へ返すのはintであり、RCWではありません。RCW(Runtime Callable Wrapper)は.NETからCOMオブジェクトを利用するためのラッパーです。COM参照を包むものなので、計測結果のintのような独立した値とは区別します。[rcw]
汎用のワーカーと業務用の入口を分けるのは、UIからInvokeAsyncへ任意の処理を渡せると、COM参照を返したり、内部のDispatcherを操作したりできてしまうためです。汎用のワーカーは内部実装に留め、StaInstrumentから必要な操作だけを公開します。
ワーカーの骨格を作る
骨格はSTAに設定した専用スレッド、COMの初期化、メッセージループの三つでできています。
まず、専用スレッドを開始前にSTAへ設定します。既存のUIスレッドの設定を変えたり、スレッドプールのスレッドを流用したりはしません。
_thread = new Thread(() => Run(factory))
{
IsBackground = true,
Name = "Owned COM STA"
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
スレッドの中のRunはCOMを初期化し、そのスレッドのDispatcherへCOMの生成を登録してから、メッセージループに入ります。次は配布ソースのRunから、例外処理と後片付けを除いた要点です。
// StaWorker.Runの要点(例外処理と後片付けは省略)
Marshal.ThrowExceptionForHR(ComApartment.CoInitializeEx(IntPtr.Zero, 2));
_dispatcher = Dispatcher.CurrentDispatcher;
_dispatcher.BeginInvoke(new Action(() =>
{
_state = factory() ?? throw new InvalidOperationException("Factory returned null.");
lock (_gate) _accepting = true;
_ready.TrySetResult(this);
}));
Dispatcher.Run();
CoInitializeExの2はCOINIT_APARTMENTTHREADEDで、このスレッドをSTAとして初期化します。[coinit] 成功を確認し、最後に対応するCoUninitializeを呼びます。既に同じ方式で初期化済みを示すS_FALSEも成功なので、その場合も自分の呼び出しと対になるCoUninitializeを呼びます。終了時の解放とCOM終了処理は同じスレッドで行います。[initialize][uninitialize]
factoryは作成済みのCOMではなく作り方を表す関数です。呼び出し側は次のように渡します。サンプルではStaInstrument.StartAsyncがこの形で呼んでいます。
var worker = await StaWorker<ProbeSession>.StartAsync(
() => new ProbeSession(publish));
この関数を専用STAの上で呼ぶので、ProbeSessionのコンストラクターが作るCOMもそのスレッドに属します。サンプルのProbeSessionは生成したスレッド以外からReadや解放を呼ぶと例外にします。
最後のDispatcher.Run()がそのスレッドのメッセージループを動かします。[dispatcher] メッセージループはスレッドに届いたメッセージを取り出して処理する繰り返しで、業務要求を保存するだけのキューとは別の仕組みです。STAは別アパートメントからの呼び出しなどをメッセージ経由で受け取るため、待機中もこのループが要ります。これが約束事の3です。[sta-loop] DispatcherはWPFで特定のスレッドに関連付けられた処理キューを管理するオブジェクトで、ここではループの実装として借りています。[ui]
flowchart TB
accTitle: COMの利用期間を専用STAのループ内に収める
accDescr: スレッドをSTAに設定しCOMを初期化した後、Dispatcherを回して生成と要求処理を行います。解放後にループとスレッドを終了します。
A["STA設定とCOM初期化"] --> B["Dispatcherを開始"]
B --> C["COM生成・要求処理・解放"]
C --> D["ループ終了・COM終了処理"]
図4: ワーカーへの要求がない間も、STAはメッセージを処理できる状態で待機します。
キューが空になるたびにWaitOneなどでこのスレッドを止める方式は採りません。また、同期COM呼び出しの間にどのメッセージが処理されるかは、呼び出し先にも依存します。Dispatcherを置くことと、処理中にも常にすべてが動けることは別です。
これで、COMは専用STAで作られ、画面は値を受け取れるようになりました。要求をDispatcherへそのまま渡せば、ReadAsyncも動きます。ただし、この骨格のままでは、計測中に次の要求が届いたとき、COMイベントを画面へ知らせるとき、画面を閉じるときの三つの場面で問題が起きます。
計測中に届いた次の要求を待たせる
一回目の計測中に、ユーザーがもう一度ボタンを押したとします。要求をDispatcherへそのまま渡す骨格でも、普段は順番に動きます。しかし、COM呼び出しの中でメッセージが処理されると、まだ戻っていない一回目の途中へ、二回目の要求が入り込むことがあります。
これが再入です。ある処理が戻る前に同じオブジェクトの処理へ再び入ることを指し、複数スレッドでの同時実行とは異なります。STAは一スレッドですが、一スレッドであることは、処理の途中へ入り直さない保証ではありません。[reentrancy]
flowchart TB
accTitle: 要求をDispatcherへそのまま渡すと計測の途中に次の要求が入る
accDescr: 計測1のCOM呼び出し中に計測2がDispatcherへ届き、呼び出し中にメッセージが処理されると、計測1が戻る前に計測2が始まります。
A["計測1がCOM呼び出しを開始"] --> B["計測2がDispatcherへ届く"]
B --> C["呼び出し中にメッセージを処理"]
C --> D["計測1が戻る前に計測2が開始"]
図5: 一本のスレッドでも、メッセージ処理を通じて次の要求が途中へ入り込みます。
機器が一回ずつしか計測できないなら、二回目は一回目が戻るまで待たせる必要があります。サンプルのStaWorker<T>は自前の要求キューを持ち、次の処理をDispatcherへ登録する条件を絞っています。
if (_busy || _scheduled || _terminated.Task.IsCompleted || _dispatcher is null)
return;
_scheduled = true;
var op = _dispatcher.BeginInvoke(
DispatcherPriority.Normal, new Action(DrainOne));
一件取り出したら_busyを立て、同期処理が戻るまで下ろしません。その間に要求が来ても、キューへ入れるだけです。COMを呼ぶ時点では、キュー操作用のlockを既に抜けています。
flowchart TB
accTitle: メッセージ処理が再入しても次の要求は待機する
accDescr: 一件目のCOM呼び出し中に二件目が届いても自前キューに留め、一件目の戻りを確認してから次をDispatcherへ登録します。
A["計測1:実行中"] --> B["busyを保持"]
Q["計測2:要求到着"] --> W["自前キューで待機"]
B --> E["計測1が戻る"]
E --> N["計測2を登録"]
W --> N
図6: 防ぐのは自分のキューの二重実行です。COM自身のコールバックの再入を消す仕組みではありません。
要求は同期のFunc<T, R>に限定します。中でasync処理を始めてTaskだけ返すと、COM操作が終わる前に次の要求へ進めてしまうからです。同じワーカーへ追加した要求を、そのSTA上で同期的に待つことも禁止します。
結果用のTaskCompletionSourceにはRunContinuationsAsynchronouslyを指定し、結果を確定する箇所で呼び出し側の継続処理まで実行しないようにしています。待機キューの上限は既定64件で、満杯なら例外にします。無制限に受け付けて遅延をためる代わりの、サンプルとしての選択です。[continuations]
COMイベントの値は画面へ予約して渡す
計測中にCOMからイベントが届いたとします。その受け手からUIへ同期的にInvokeし、UI側もワーカーの完了を同期的に待っていたら、互いに相手を待って止まります。
flowchart TB
accTitle: イベントからUIを同期的に呼ぶと互いに待つ
accDescr: 専用STAはイベントの受け手からUIへのInvokeの完了を待ち、UIスレッドはワーカーの完了を同期的に待つため、どちらも進めません。
S["専用STA:UIへのInvokeの完了を待つ"] --> U["UIスレッド:ワーカーの完了を待つ"]
U --> S
図7: 同期的な待ちが両方向にそろうと、どちらのスレッドも進めません。
画面への通知はPostなどで予約し、COM側へ速やかに戻します。Invokeは同期呼び出しで、BeginInvokeやInvokeAsyncは処理を登録してすぐに戻る非同期の呼び出しです。[ui]
サンプルでは、この予約をUiMailboxが受け持ちます。UI側で、UIのSynchronizationContextと表示処理を渡してUiMailboxを作ります。
public void Publish(int value)
{
if (Volatile.Read(ref _closed) != 0) return;
context.Post(_ =>
{
if (Volatile.Read(ref _closed) == 0) consume(value);
}, null);
}
flowchart TB
accTitle: UIへの通知を予約した後でも閉じた画面を避ける
accDescr: COMの受信処理は値をPostして戻り、UIで実際に通知を実行するときにも終了状態を確認します。
A["COMイベント:値を受信"] --> B["UIへPostして戻る"]
B --> C["UIで実行する時に再確認"]
C --> D["閉じていれば破棄"]
C --> E["開いていれば表示"]
図8: Postした直後に画面が閉じることもあるため、受信時だけでなく配信時にも確認します。
通知する値はコピー済みのintです。配列なら共有してよい内容かを確かめ、必要なら所有権を切り離してから渡します。
画面を閉じ始めたら、UI上でメールボックスを無効にします。既にUIキューへ載った通知にも、この判定は当てはまります。ネイティブへ返るコールバックから.NETの例外を漏らさないよう、サンプルの受信クラスは例外を保存します。実製品では監視・ログへの報告も設計してください。
画面を閉じる前にCOMの後片付けを終える
サンプルの専用スレッドはIsBackground = trueです。背景スレッドはプロセスの終了を妨げず、最後の前面スレッドが終わると、システムは残った背景スレッドを止めてプロセスを終了します。[background] 画面を閉じるときに何もしなければ、COMの解放もCOMの終了処理も済まないうちに、ワーカーのスレッドごと止められることがあります。
先にDispatcherが止まっても、サンプルではCOMの後片付けは行われます。RunはDispatcher.Run()が返ったあとも、同じSTAのfinallyでCOMを解放してCoUninitializeを呼ぶためです。CoUninitializeは残った通信があれば、自分でモーダルなメッセージループを回してメッセージを処理します。[uninitialize] ただし、StopAsyncを通らずにループが終わると、受け付け済みでまだ実行していない要求は実行されずに例外で終わります。終了の順番を決めているのは、受け付けた要求を処理し終えてから解放するためです。
ワーカーはStopAsyncで終了します。受付を閉じ、受理済みの要求を終えた後、同じSTAでイベントを解除し、COMを解放します。それからDispatcherを終了し、スレッドが終わったことまで待ちます。
flowchart TB
accTitle: 受付停止からスレッド終了までを待つ
accDescr: 新規要求を拒否し、既に受け付けた要求を処理してからイベントを解除しCOMを解放します。ループとスレッドの終了後にStopAsyncが完了します。
A["新規受付を停止"] --> B["受理済み要求を完了"]
B --> C["イベント解除・COM解放"]
C --> D["Dispatcherを終了"]
D --> E["COM終了処理・スレッド終了"]
図9: 受け付けた要求を処理し終えてからCOMを解放し、そのあとでDispatcherを止めます。
StopAsyncを何度呼んでも同じTaskが返り、解放は一度です。終了中の新規要求は失敗します。単独の読み取り失敗はそのTaskへ返し、異常なDispatcher終了では待機中のTaskにも例外を設定します。呼び出し側が「永遠に結果待ち」のまま取り残されないためです。
画面のClosingイベントでは一度クローズを取り消してawaitします。.Wait()や.ResultでUIスレッドを止めて待つことはしません。
サンプルのDemoWindowはクローズを取り消した後にメールボックスを無効にし、StopAsyncを待ち終えてから改めて閉じます。初期化中に閉じられた場合も同じ初期化Taskを待ち、生成された所有者を止めます。終了時のエラーは表示して記録できるようにし、スレッド終了が確認された後なら再度閉じられます。
flowchart TB
accTitle: 終了処理を待ってから画面を閉じ直す
accDescr: Closingでクローズを取り消してメールボックスを無効にし、StopAsyncを待った後に閉じ直します。失敗した場合はエラーを表示し、もう一度閉じられるようにします。
A["Closing:クローズを取り消す"] --> B["メールボックスを無効化"]
B --> C["StopAsyncをawait"]
C --> D["成功:閉じ直す"]
C --> E["失敗:エラーを表示"]
E --> F["再度閉じられる"]
図10: StopAsyncが終わるまで画面を閉じないので、COMの後片付けより先に画面が消えることはありません。
サンプルで動作を確かめる
配布サンプルではここまでの部品を次の画面と検証用COMにつないで動かします。どちらもサンプル固有の部分で、実製品ではアプリの画面とメーカー製COMに置き換わります。
DemoWindowは、WPF画面からStaInstrumentを使う例です。読み込み時にStaInstrument.StartAsync、「Read 21」ボタンでReadAsync(21)、ClosingイベントでStopAsyncを呼びます。COMイベントで届いた値はUiMailbox経由で表示します。
StaProbe.dllはメーカー製COMの代わりに使う検証用COMです。実機依存を避けるため、C++で小さなIUnknown実装を用意し、ネイティブ側が保持するC#の受け手へコールバックさせています。C#だけで書いたモックではありません。
Readは受け取った値の2倍を返し、同じ値をイベントとしても通知します。所有スレッド以外からのReadやイベントの登録はRPC_E_WRONG_THREADで拒否します。
一方、ベンダー製品、OCXの画面埋め込み、COM登録、別アパートメント間のProxy/Stubを試験したものではありません。
ビルドと実行
ソース一式(ZIP)を展開してください。.NET 8 SDKとVisual StudioのC++ビルドツール(Windows SDKを含む)を用意し、PowerShellから次を実行します。ネイティブ側・C#側ともx64です。レジストリへのCOM登録や管理者権限での登録は行いません。
-
ビルドと自己テストを実行し、続けてWPFの操作例を起動します。
cd .\sta-com-worker .\build-test.ps1 # テスト後、同じフォルダーからWPFの操作例を起動 dotnet run --project StaComWorker.csproj -c Release -- --demo
ボタンはReadAsync(21)を呼び、検証用COMが返す42を表示します。
自己テストで確認していること
自動テストでは次を確認します。実行時の.NET・OS情報とケースごとの結果はsta-test-report.jsonへ出力します。
| 確認対象 | 壊した条件と期待する結果 |
|---|---|
| スレッドと寿命 | ネイティブの生成・Read・最終Releaseが同じスレッド。残存オブジェクト数は0。 |
| 再入 | テスト用に入れ子のメッセージ処理を起こしても、次の要求は始まらない。 |
| 終了の競合 | 一件実行中・一件待機中でStop。二件を完了し、新しい要求を拒否する。 |
| 異常系 | 生成失敗、読み取り例外、解放例外、予期しないループ終了をTaskで観測できる。 |
| UI通知 | 予約後に画面を閉じた通知を捨て、同期Sendを使わない。 |
失敗したケースはレポートの.NET・OS情報、エラーと該当する処理を照合してください。終了を待てない場合に、待ちだけを打ち切って解放済みと扱わないでください。
実製品へ移す前に確認すること
専用STAは所有する場所を限定する仕組みであり、任意のCOM製品の移動・解放・強制停止を保証しません。メーカー製品に差し替えた場合は、同じ観点の試験をその製品でも追加します。特に次の三点は製品ごとに確かめてください。
RCWを解放してよい条件
サンプルは新規作成して誰とも共有しない検証用RCWだけを持ち、イベント解除後にReleaseComObjectします。これは共有RCWを一律に解放する推奨ではありません。別の利用者がいるRCWを切り離す危険は、Microsoftの実例にもあります。[release]
ベンダーへ差し替える際は、取得したオブジェクトの所有権、専用のClose処理、イベント解除後に通知が残るかを確認します。初期化に失敗するアダプター自身にも、途中まで取得した資源を戻す責任があります。ループで参照数を強制的にゼロにするFinalReleaseComObjectや、終了のたびのGC.Collectで辻褄を合わせる設計にはしません。
別スレッドから届くイベント
この検証用COMは所有スレッドへ同期的にコールバックします。別スレッドから通知するベンダー製品では、イベント引数の寿命やマーシャリングを確認し、別スレッドからワーカー内部の状態を直接操作しないようにします。
戻らない呼び出しと自動復旧
StopAsyncは同期呼び出しを強制中断しません。タイムアウト付きで待つWaitAsyncも元の処理を止める操作ではありません。試験では、待ちだけをタイムアウトさせた後も元の処理が残ることを確認し、試験側からブロックを解除して片付けます。[timeout]
flowchart TB
accTitle: 待つのをやめても元のCOM処理は残る
accDescr: 呼び出し側のタイムアウトと、専用STA内の同期処理の終了は別です。処理が戻るまでワーカーの安全な終了は待ちます。
A["呼び出し側:待ちがタイムアウト"]
B["専用STA:COM処理は実行中"] --> C["戻らなければStopも未完了"]
A -.-> B
図11: 背景スレッドを強制終了して、COMを安全に片付けたことにはできません。
ベンダーが保証する中断・タイムアウトAPIがないのに、自動復旧まで必要なら、専用STAだけでは足りません。別プロセスへ分離して終了を管理する構成を検討します。処理を再送する際に二重実行してよいかも別途決めます。
関連記事
参考資料
確認日:2026年9月15日。Windowsの規則と、このサンプルで選んだキュー・終了方針は区別しています。コードの動作確認は配布サンプルの自己テストで再実行できます。
用語の定義に使うCOM・STA・RCW・WPFの資料は2026年9月18日に再確認しました。2026年10月18日の改稿ではCOMの初期化、COINIT、背景スレッドの資料を確認して追加しました。いずれの改稿もWindows上のサンプルやメーカー製品の実機試験を実施したという意味ではありません。
- Microsoft Learn、Single-Threaded Apartments。参照箇所は本文の規則一覧と、その後のアパートメント間のインターフェース受け渡しの段落。対象オブジェクトの所有スレッドで呼び出しが実行されること、境界を越えるときにマーシャリングが必要なことを参照しています。冒頭のasync Mainの短い例を本サンプルの初期化手順としては使っていません。
- Microsoft Learn、Single-Threaded Apartmentsの、隠しウィンドウ
OleMainThreadWndClassとGetMessage/DispatchMessageを説明する段落。本文のメッセージループの必要性の根拠です。特定の待機APIが常にメッセージを一切処理しない、という一般化はしていません。 - Microsoft Learn、Initializing the COM Library。アパートメントスレッドを指定したときに守る二つの条件(各COMオブジェクトを一つのスレッドから使うこと、そのスレッドがメッセージループを持つこと)を参照しています。
- Microsoft Visual Studio Team、Marshal.ReleaseComObject Considered Dangerous(2010-03-01)。節「Calling COM objects from Managed code」と「Implementing COM interfaces on managed objects」。RCWとCCW、およびネイティブから保持した受け手の寿命の説明を参照しています。
- Microsoft Learn、Dispatcher.Run、Remarks。現在のスレッドのDispatcherに対するメッセージループを開始することを参照しています。
- Microsoft Learn、COINIT enumeration、Constants。
COINIT_APARTMENTTHREADEDの値が0x2で、スレッドをアパートメントスレッドとして初期化することを参照しています。 - Microsoft Learn、CoInitializeEx、Return valueとRemarksの最初の二段落。S_OK/S_FALSEと、成功した呼び出しに対応するCoUninitializeを参照しています。
- Microsoft Learn、CoUninitialize、Remarks。成功した初期化ごと(S_FALSEを含む)に一度呼ぶこと、主メッセージループを抜けたあとに呼ぶこと、残った通信があれば自身のモーダルループでメッセージを処理することを参照しています。
- Microsoft Learn、Single-Threaded Apartments、末尾の再入を説明する二段落。ページ内検索語は
reentered。同じSTAでもメッセージ処理や別スレッドへのORPCで再入できることが根拠です。busyによる業務要求の直列化は、この規則を踏まえた本サンプルの設計です。 - Microsoft Learn、TaskCreationOptions、FieldsのRunContinuationsAsynchronously。継続処理を非同期実行にする指定を参照しています。キュー上限64件はMicrosoftの推奨値ではなく、本サンプルの既定値です。
- Microsoft Learn、Threading model — WPF、節「Overview and the dispatcher」。Invokeが同期呼び出し、BeginInvoke/InvokeAsyncが非同期登録であること、UIを所有するスレッドの説明を参照しています。
- Microsoft Learn、Foreground and background threads、冒頭の段落。前面スレッドがすべて終わると、システムが背景スレッドを止めてプロセスを終了することを参照しています。
- Microsoft Visual Studio Team、Marshal.ReleaseComObject Considered Dangerous、節「Marshal.ReleaseComObject – the silent assassin」。共有RCWを解放すると別の呼び出し側も使えなくなる例を参照しています。ここでの一回の解放は、検証用オブジェクトを独占していることが前提です。
- Microsoft .NET Team、How do I cancel non-cancelable async operations?、冒頭の三種類のキャンセルの区別と、待ちだけを取り消した後も元の処理の後片付けが必要という説明。WaitAsyncの動作はサンプルでも比較します。
- Microsoft Learn、The Component Object Model、冒頭のバイナリ標準とインターフェースを説明する段落。「画面が固まる理由」のCOMの説明を参照しています。
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
クリップボードとドラッグ&ドロップの仕組み ── OLEデータ転送を業務アプリで正しく扱う
Excelの表を貼ると崩れる・コピー元を閉じると貼れない――正体は同じ内容を複数形式で置くクリップボードの仕組みです。標準形式・遅延レンダリング・OLEドラッグ&ドロップから履歴とクラウド同期のポリシーまで解説します。
WinForms / WPFアプリのCI/CD実践 ── GitHub Actionsでビルドから署名・配布まで自動化する
WinForms / WPFアプリのCI/CDをGitHub Actionsで組む実務ガイド。windows-latestでのビルド+テストの最小YAML、タグ駆動のバージョン採番、signtoolによる署名の組み込み、MSI/MSIX/ClickOnce/xcopy別のC...
Arm版Windowsで業務アプリは動くのか ── x64エミュレーション(Prism)とネイティブDLL・COMの現実
「Arm版Windowsで業務アプリは動くのか」に開発者・情シス向けに答えます。x64エミュレーション(Prism)の仕組み、ドライバーなど動かない層、.NETのAnyCPUとP/Invokeの問題、Arm対応チェックリストまで整理します。
Windowsアプリのタスクトレイ常駐とトースト通知 ── NotifyIconの落とし穴とAppNotificationの選び方
業務Windowsアプリのタスクトレイ常駐とトースト通知の実装を整理します。NotifyIconの正しい使い方、Explorer再起動時の再登録、トーストAPI3種の選定判断表、通知が届かないケースまで解説します。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
ActiveX / 移行テーマ
COM / ActiveX / OCX を残すか、包むか、置き換えるかを整理するトピックです。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
既存資産活用・移行支援
COM / ActiveX / OCX、32bit / 64bit 制約を抱える既存資産の活用と移行を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- STAのCOMをTask.Runに渡せばUIは止まらなくなりますか?
- Task.RunはCOMオブジェクトの所属アパートメントを移しません。UIで作ったSTAオブジェクトへの呼び出しは、マーシャリングによって元のSTAへ戻る場合があります。専用STAで使える非表示のコンポーネントなら、生成から利用・解放までをその専用スレッドにまとめる方法が選べます。
- SetApartmentState(STA)だけでは足りませんか?
- STAには別アパートメントからの呼び出しなどを処理するメッセージループも必要です。サンプルは専用スレッドでDispatcher.Runを使います。STAに設定したあと、要求待ちのためにそのスレッドをブロックするだけの実装とは異なります。
- 専用STAなら再入は起きませんか?
- 起こり得ます。COM呼び出し中などにメッセージが処理されると、同じスレッドでもコールバックへ再入できます。サンプルでは実行中フラグにより次の業務要求を開始しませんが、COM自身の再入まで禁止するものではありません。イベント処理は短くし、UIの完了を同期的に待たない設計にします。
- StopAsyncは実行中のCOM呼び出しを中断しますか?
- 中断しません。新規要求を拒否し、受理済みの処理を終えてからイベント解除と解放を行い、スレッドの終了まで待つ設計です。COM呼び出しが戻らなければ終了も待ち続けます。強制的な復旧が必要なら、ベンダーの中断APIや別プロセス化を検討します。
- ReleaseComObjectをどのCOMにも使ってよいですか?
- いいえ。共有されたRCWを解放すると、別の利用者まで使えなくなります。サンプルは新規作成して外へ渡さない検証用RCWだけを所有し、その前提で解放します。ベンダー製品へ差し替える際は、所有権と終了API、イベント解除の条件を別途確認してください。