syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

C/C++の#warning

コンパイルエラーを意図的に発生させる#errorプリプロセッサディレクティブに関しては、お馴染みの#includeや#defineなどと同様に言語仕様として標準化されているようで、おそらくすべてのC/C++処理系でサポートされています。

#ifdef __cplusplus
#error This code does not support C++!!
#endif

問題は警告のほうです。C/C++でユーザー定義の警告を発生させる方法は標準化されていません。
gccには#warningディレクティブが存在するのですが、Visual C++には存在しません。Clangにも存在しないようです。
#warningの実装は過去に提案されていたりするのですが、実現には至っていないようです。

#pragma messageはたいていの処理系でサポートされているのですが、単にメッセージを出力するだけで、警告にはならず、強制力がありません。

#ifdef NDEBUG
#pragma message("This code is compiled when release mode.")
#endif

また、#pragma messageでファイル名と行番号を出力しようとすると、__FILE__マクロと__LINE__マクロおよびトークンの文字列化と結合を駆使せねばならず、移植性のあるコードを記述するのが大変になります。

C#

一方、C#では#warningがちゃんと標準化されています。当然#errorもあります。

やはり本来は処理系ごとにどうにかするべき問題ではなく、C#のように言語仕様として標準化してほしい機能です。


え、Javaですか? そんな貧弱な言語は知りませんね。窓から投げ捨てましょう。

※2023-02-19追記:
C23およびC++23で、ついに#warningが標準化されるようです。

C#の登場から20年以上の歳月を費やし、ようやくその必要性を認めたようです。相変わらず遅すぎる。

Windowsの画面キャプチャ取得方法

Windowsにおいて、画面のキャプチャ(スクリーンショット)を取得する方法はいくつかあるのですが、下記の標準機能は遥か昔(たぶんWindows 95あたり)から搭載されています。いずれもWindowsユーザーであれば誰もが知っていないとおかしいレベルの、初歩中の初歩です。

PrintScreenキー デスクトップ全体を1つの画像として取得します。マルチディスプレイ(マルチモニター)環境の場合、合成された1つの画像となります。
Alt+PrintScreenキー アクティブなウィンドウのみのスクリーンショットを取得します。ただしMDI子ウィンドウのみのキャプチャには対応していません。アクティブでない別のウィンドウがかぶさっている場合、そのウィンドウも映り込みます。

PrintScreenキーで取得したデータは、不可視のクリップボード(システム全体で共有するグローバルメモリ領域)にビットマップ画像データとして保存されます。オンメモリなので、当然Windowsをシャットダウン/再起動すると消失する揮発性のデータです。クリップボードに保存されたビットマップ画像データをファイルとして保存する場合は、お好みのペイントツールにてキャンバスに貼り付けて、所望のフォーマットで保存します。Windowsに標準搭載されているMS Paint(ペイント)を使う場合は、あらかじめキャンバスサイズを小さくしておけば、ペースト時に画像サイズに合わせてキャンバスを自動拡張してくれます。MS Word/Excel/PowerPointなど、Office系のソフトも直接クリップボード経由でドキュメント内に画像を貼り付けることができますが、設定によってはファイル埋め込みの際に、勝手に画像の解像度を落としてしまうこともあるので注意が必要です。サードパーティ製のアプリケーションを自由にインストールできる自宅のプライベート端末では、起動と動作が軽快なIrfanViewを使ってクリップボードデータをファイル保存することが多いです。

ちなみにスクリーンショットを画像ファイルとして保存するときは、通例可逆圧縮PNGフォーマットを使います。非可逆圧縮JPEGは主に写真向けの圧縮フォーマットであり、画面スクリーンショットの保存には向いていません(色変化の激しい部分でモスキートノイズが目立ったり、PNGよりもファイルサイズが増加したりします)。

なお、スクリーンショット画像にはマウスなどポインティングデバイスのカーソルが含まれません。カーソル合成前の画像となります。特に困ることはないと思いますが、もしマニュアル作成用途などで画像にカーソルをどうしても含めたい場合は、スクリーンショットにカーソルのアイコンをオーバーレイ描画した合成画像を自動生成できるツールがあるので、そういったものを利用する方法もあります。

Windows Vista以降

Windows Vistaではキャプチャ用のSnipping Toolが搭載されました。デスクトップ全体・指定ウィンドウ以外に、選択領域のキャプチャもできます。しかし、個人的には上記のPrintScreenキーとペイントツールを使った方法よりもかえってめんどくさいので使っていません。また、PrintScreenキーを使った方法であれば、コンボボックスのドロップダウンリストを展開表示した状態や、特定のUI要素をマウスオーバーした状態でスクリーンショットを取得することができますが、Snipping Toolではそういったことができません。

Windows 8以降

Windows 8において、Windowsキー+PrintScreenキーを押すと、"%UserProfile%/Pictures/Screenshots" にデスクトップ全体のスクリーンショットPNG形式で自動保存されるようになりました。これはわりと有用な機能だと思いますが、Windows 7以前では使えないので、知っている人は少ないかもしれません。

ただし、Alt+PrintScreenのようにアクティブウィンドウのみをキャプチャして直接ファイル保存する機能がありません。この点がかなり不満です。

Windows 10

Windows 10ではゲーム録画 (Game DVR) の機能が追加され、Windows+Alt+PrintScreenでアプリのスクリーンショットを取得・保存できるようになりました。保存場所は"%UserProfile%/Videos/Captures"だそうです。「ゲーム録画」と銘打っていますが、ゲームアプリ以外でも使えます。しかし、利用にはXboxアプリへのMicrosoftアカウントを利用したサインイン(ネットワーク接続)が必要です。ローカルアカウント中心で使っているユーザーや、企業ユーザーの場合は使えない手段でしょう。Game DVRは当初既定で無効化されていたものの、Anniversary Update (1607) にて既定で有効化されたようです。以降は無効化する場合にもXboxアプリへのサインインが必要だそうで、もう意味不明ですね。Game DVRは個人情報が勝手にぶっこ抜かれる可能性があるので、自分は使っていません。

なお、Creators Update (1703) では余計な機能満載のPaint 3Dが標準インストールされるようになったのですが、さらにFall Creators Update (1709) では、従来の標準ペイントツールMS Paintが廃止・非標準になりました。完全に廃止されたわけではなく、Windowsストア(Microsoftストア)から明示的にインストールすれば使えるらしいのですが、ストアへのアクセスにはやはりサインイン(ネットワーク接続)が必要となります。電卓アプリや画像ビューアーの劣化と同じような道をたどるようです。ぶっちゃけ「Creators Update」とかいいつつ、誰も望んでいない、使えないゴミアプリを搭載しただけのアップデートです。そもそもCreatorだったらAdobe/Autodeskなどのデファクトスタンダードサードパーティ製ツールを使うのでPaint 3Dなんぞ要らないですね。

MS Paint自体はAdobe Photoshopなどと比べると大した機能を持っていませんが、これまでは標準インストールされていたことが最大の強みでした。特に仕事で、好きなペイントツールが使えない(インストールされていない/できない)端末において、取得したスクリーンショットをファイル保存するときだけはMS Paintをよく使っていました。また、機能が少ないぶんシンプルで、操作体系も昔からほとんど変わっていないため、初心者でも使いやすいというメリットがありました。しかし、今後は仕事で他の人にスクリーンショットの取得を指示する場合、まずはPaint 3DもしくはSnipping Toolに慣れてもらう必要がありそうです。

Steam

Valveが提供・運営するSteamプラットフォーム向けのゲームでは、共通してF12キーでスクリーンショット画像をファイル保存できます。ウィンドウモードでもクライアント領域のみが保存されるので、Steamの場合はこちらのほうが便利でしょう。昔はJPEGフォーマットのみでしたが、現在は劣化の無いPNGでの保存もサポートされています。
保存場所は "%ProgramFiles(x86)%/Steam/userdata/<ユーザーID>/760/remote/<作品ID>/screenshots" です。階層が深くて覚えにくいので、途中の "remote" フォルダーへのショートカットを作成しておくといいかもしれません。
Steamクライアントの「ライブラリ」から各ゲームのページを表示して、「スクリーンショット」欄の「全スクリーンショットを表示」ボタンをクリックすることで、「スクリーンショットアップローダ」ダイアログが表示され、このダイアログ上の「フォルダを表示」ボタンを押すことでもアクセスできます。ダイアログ上で各スクリーンショット画像(とサムネイルのペア)を削除したり、オンラインコミュニティにアップロードしたりすることができます。

余談

XPやVista/7はLunaやAeroといったVisualテーマを適用すると、ウィンドウの四隅が丸くなります。キャプチャすると四隅の部分は白色となりますが、個人的にはこの丸まった四隅がダサくて大嫌いでした。
また、過去のWindowsではタイトルバーにグラデーションが使われていたり、Windows Aero (Aero Glass) の機能でウィンドウが透過していたりしたのですが、これによりPNGキャプチャ画像は無駄にファイルサイズが大きくなりがちでした。なめらかに色変化する複雑な画像はPNGだと圧縮しづらいからです。Windows 8.xではデザイン方針としてModern UIを採用することでタイトルバーがシンプルな単色になり、PNGキャプチャ画像のファイルサイズも削減されました。しかし、Windows 10 1709では、Fluent Design Systemという、これまたMSの自己満足的デザイン方針が採用され、AppleiOSで採用されているような半透明のすりガラス(アクリル)効果が多用されることになったため、再びPNGキャプチャ画像のファイルサイズが無駄に増えそうです。「設定」→「個人用設定」→「色」にて「透明効果」をOFFにすることで無効化できるので、気になる人は設定変更しておいたほうがいいと思います。

2018-07-31追記:
Windows 10の次期バージョン(RS5)では、Snipping Toolが廃止予定となったようです。代わりに「Screen Sketch」とやらが導入される予定だそうですが、どうも微妙な感じのツールですね。どうせ使わない気がします。短命に終わる要らないアプリを開発してるヒマがあったら、バグのひとつでも取り除いてOS自体の品質を高めて欲しいです。

GeForceドライバー380系列のDirect3D 11バグ

3DグラフィックスとC++の研究目的で、DirectX 11 (Direct3D 11) を使った自前FBXビューアーを開発しているのですが、とある自作FBXファイル(約18,000ポリゴン程度)を開いて、カメラを回転させながら描画すると、レンダリングが停止する現象に遭遇しました。デバッグ レイヤーからは以下のようなエラーメッセージが出ます。いわゆるTDRハングアップです。

D3D11: Removing Device.
D3D11 ERROR: ID3D11Device::RemoveDevice: Device removal has been triggered for the following reason (DXGI_ERROR_DEVICE_HUNG: The Device took an unreasonable amount of time to execute its commands, or the hardware crashed/hung. As a result, the TDR (Timeout Detection and Recovery) mechanism has been triggered. The current Device Context was executing commands when the hang occurred. The application may want to respawn and fallback to less aggressive use of the display hardware). [ EXECUTION ERROR #378: DEVICE_REMOVAL_PROCESS_AT_FAULT]

具体的にどのメソッドコールやシェーダーがタイムアウトのトリガーになっているのかまでは調べ切れていないのですが、以前は同じFBXファイルをまったく問題なく表示できていました。検証した組み合わせ環境は以下の通りですが、どうやらNVIDIA GeForceドライバーを384.94に更新したことが原因のようです。Windows 7 (SP1 Platform Update) でもWindows 10でも発生します。

  • Win10 (1703) x64 + GeForce GTX 760 4GB + 384.94: hang
  • Win10 (1703) x64 + Quadro M4000 + 382.48: OK
  • Win10 (1703) x64 + Quadro M4000 + 386.01: OK

少なくともQuadro M4000では比較的新しいドライバーを適用しても問題が発生しないことを確認できています。Quadroのドライバーは基本的にGeForceよりも厳密度や品質・安定性が重視されているので当然と言えば当然かもしれません。Kepler固有の現象なのか、それともGeForceであればMaxwellPascalでも発生するのかは不明です。

Direct3D 11は現在のハイエンド3Dゲーム開発における中核ともいえるAPIで、すでに次世代ローレベルAPIであるDirect3D 12やVulkanが正式リリースされて2年ほど経つものの、アプリケーション開発のしやすさの点から言えば、いまだDirect3D 11の地位は揺るぎません。今回発見したバグは、現役のAPIに関する基本的なリグレッションなので、すでにゲーム開発者やゲーマーからNVIDIAにバグ報告が寄せられて修正されていてもいいような気がしますが、2017年12月にリリースされたドライバーでも修正されていないようです。

逆にある程度古いドライバーだとタイムアウト現象は確かに出ないのですが、古いドライバーにはセキュリティ脆弱性が潜んでいることがあるので、古いドライバーを使い続けるのも得策ではありません。もともと384.94は、重大なセキュリティ脆弱性が修正されたとかいう話だったので急遽インストールしたものです。

なお、年明け早々に大々的に報じられたCPU脆弱性Spectre/Meltdownのうち、Spectreの修正に対応した390系列のドライバーが先日公開されました。とはいってもGPU側の予測分岐機能などがシステムのセキュリティに影響を及ぼすわけではなく、たとえばWebブラウザ上で実行するJavaScriptのように、悪意のあるコードの踏み台となりえるソフトウェアプログラムが含まれていたせいか、もしくはパッチ適用によるシステム低速化を緩和させる目的で、おそらく今回の修正対象となったものと思われます。通例ドライバーはシステムメモリにカーネルモードでアクセスできるため、対策も必要となるのでしょう。近いうちに390系列も試す予定です(ただし人柱になるのは御免こうむりたいので、しばらくは様子見)。

余談:ドライバー更新時の問題

バージョン番号の新しいNVIDIAドライバーをインストール(バージョンアップ)する場合は通例上書きインストールできるものなのですが、検証などのためにロールバック(バージョンダウン)しなければならない場合、まず現在のドライバーをアンインストールするのが常套手段です。
しかし、NVIDIAのグラフィックスドライバーおよびHDオーディオドライバー*1をコントロールパネルからアンインストールした後、Windowsを再起動すると、Windowsによって勝手にハードウェアの認識と古いデフォルトドライバーのインストールが始まってしまうのが厄介です(しかも結構時間がかかる)。マウスやキーボード、フラッシュメモリなどのUSBデバイスに関しては、こういったプラグ&プレイによる自動認識動作は非常にありがたいのですが、グラフィックスやオーディオまで勝手にデフォルトドライバーをインストールされると非常に困ります。
なお、XPまではグラフィックスとオーディオのドライバーは自動インストールされず、OSインストール直後はハードウェアアクセラレーションが使えない状態となるのがデフォルト動作になっていました。このグラフィックスとオーディオのデフォルトドライバー自動インストール動作はWindows Vista以降で実装されたものです。ドライバーのインストール作業の方法が分からないビギナーにとってはありがたいものなのかもしれませんが、ドライバーバージョンを完全に自分でコントロールしたいパワーユーザーにとっては単なるおせっかいでしかない迷惑機能です。「デバイスのインストール設定」でWindows Update経由のドライバーインストールを無効化していても抑制できない模様で、またローカルのどこかにデフォルトドライバーのインストーラーパッケージを隠し持っているためか、ネットワークを切断していても抑制できません。どうやら抑制するにはレジストリ操作が必要らしく、面倒なので今回はあきらめることにしました。

*1:HDMIなど、グラフィックス以外にオーディオ伝送も可能とするインターフェイスが存在します。そういったものをグラフィックスデバイス経由でサポートする目的で、NVIDIAのドライバースイートにはオーディオドライバーも含まれています。

OpenCL/OpenGL/OpenCVのバイナリキャッシュ機能は使ってはいけない

OpenCL/OpenGLには当初、カーネルおよびシェーダープログラムに関してSPIR/SPIR-Vのような中間表現(バイトコード)規格が用意されておらず、それゆえオフラインコンパイルがサポートされていませんでしたが、コンパイル済みバイナリ(ベンダー依存)のキャッシュ機能はありました。

OpenCL 1.0:

OpenGL 4.1 or GL_ARB_get_program_binary:

また、OpenCV 2.xには、oclモジュールにおいて使用されるOpenCLカーネルのバイナリを、アプリケーションで使用する画像処理関数のカーネルごとに、初回呼び出し時に指定ディレクトリにファイル保存させることのできるキャッシュ機能が備わっていました。なお、保存されるファイル名にはOpenCLプラットフォーム名とデバイス名が含まれ、.clbの拡張子が付けられます。ただしワイド文字列のサポートはなく、したがってWindows上ではUnicodeがサポートされません*1

キャッシュ機能はOpenCV 3.0でいったんサポート外となった後、3.4にてOpenCL APIの薄いラッパーとして形を変えて復活したようです。

問題点

NVIDIA GeForceドライバーのとあるDirect3D 11リグレッション検証のために、グラフィックスドライバーのロールバック作業をしていたんですが、ついでにOpenCV 2.4.13を使ってOpenCV-CLの動作確認テストも実施したところ、OpenCLバイナリキャッシュの前方互換性がないことに気付きました。
具体的に言うと、例えば新しい388.71で出力したOpenCLカーネルのバイナリ*2を、古い353.90や364.72で読み込もうとすると、CL_INVALID_BINARYのエラーが発生します。OpenCV 2.4.13だと以下のようなエラーメッセージとなります。

XXX\sources\modules\ocl\src\cl_programcache.cpp:445: error: (-217) CL_INVALID_BINARY in function cv::ocl::ProgramFileCache::getOrBuildProgram

一応、古いカーネルを新しいドライバーで読み込むことはできる(後方互換性はある)ようですが、それが確実に保証されるのかどうか不明です。たぶん保証はされないでしょう。少なくとも前方互換性に関しては確保されていないことは確かです。おそらくOpenGLのシェーダープログラムバイナリに関しても似たような状況となっていることが予想されます。

これのどこが問題かというと、もしアプリケーションがカーネルバイナリをファイルとしてストレージにキャッシュ(保存)した後で、ユーザーがデバイスドライバーを変更すると、互換性がなくなって次回起動時にそのキャッシュが読み込めなくなり、アプリケーションが動作しなくなる、ということが懸念されるからです。
もしどうしてもバイナリキャッシュ機能を使いたい場合は、ドライバーの実装(バージョン番号)に紐づけた管理をするべきですが、少なくともOpenCV 2.xの実装はそうなっていません*3

なお、ドライバー変更以外でも、OpenCVのバージョンを変更すると、画像処理関数内部のカーネルが変わってOpenCLバイナリに互換性がなくなる、というケースがありえます。いっそOpenCV-CLのキャッシュ機能は使わないほうがよいでしょう。
少なくともPCにおけるAMD/NVIDIA環境に関しては、PCをシャットダウンするまで有効なオンメモリの組み込みキャッシュ機能がOpenGL/OpenCLドライバー側に備わっているはず*4なので、そちらに期待したほうがよいと思います。PC起動後の初回のコンパイル時間を我慢できれば、わざわざアプリケーション側でファイルとしてキャッシュする必要はありません。

ちなみにNVIDIA GeForceの場合、x86とx64とでOpenCLカーネルのバイナリ互換性がないらしいです。x86プロセスにてコンパイルしたバイナリをx64プロセスのドライバーに食わせようとすると、ドライバーがやはりCL_INVALID_BINARYのエラーを吐き、最悪の場合TDR後にドライバーがクラッシュすることもあるようです。普通に考えれば、ホストCPUアーキテクチャに依らずGPUコード側は同じはずなんですが、CUDA Unified Memory機能関連などでインターフェイス仕様の差異があるのかもしれません。Quadroは不明。
AMD FireProの場合は、少なくともCatalyst 15時点でx86/x64間の互換性がある模様です。Radeonは不明。

余談

ドライバー353.06だと、OpenCV 2.4.13のCV-CLで下記のエラーが発生します。別のバージョンのドライバーだと正常に動作するので、353.06のOpenCLドライバーにバグがある模様。

XXX\sources\modules\ocl\src\cl_operations.cpp:219: error: (-217) CL_MEM_OBJECT_ALLOCATION_FAILURE in function cv::ocl::openCLMemcpy2D

古いNVIDIAドライバーにはAPIが正常に動作しないバグがあるだけでなく、セキュリティ上の脆弱性も多々あるので、できるかぎり古いドライバーは使わないほうがよいです。
ところで先日、32bit版OS向けNVIDIAドライバーのサポートが打ち切られることが発表されましたが、現時点でWindows 7/8.1/10の32bit版は64bit版同様まだサポート期限が切れていないのに、今後32bit版において新しいハードウェアの対応がなされないのは企業ユーザーにとってかなり痛手になると思われます。32bit OS向けのセキュリティ対策は2019年1月まで続けられるそうですが、余命宣告を受けたも同然です。いずれにせよ、個人的にはどうでもいいんですけどね。

結論

やはりNVIDIA GeForceOpenCL実装は貧弱極まりないですね。また、それ以上にOpenCL/OpenGL仕様のいい加減さにあきれます。だからバイトコード規格を先に定義しろと(以下略)

*1:設計思想の古いOpenCVにとっては平常運転です。

*2:NVIDIAの場合、内部的にはCUDAで使われるPTXフォーマットになる模様です。

*3:クロスプラットフォームかつベンダー非依存な方法でドライバーのバージョン情報を取得する手段が標準化されていないこともあり、これはある意味仕方ないと思われます。

*4:OpenCL CもしくはGLSLのソースコード文字列に変更がなければ、一度コンパイルしたカーネルやシェーダープログラムのキャッシュが次回以降も使われるようです。

volatileに関してそろそろ一言いっておくか

今更言うまでもありませんが、C/C++Java/C#ではキーワードvolatileの意味が若干異なります。

C/C++

C/C++言語のvolatile修飾子は、コンパイラに副作用を示唆し、メモリアクセスの最適化を抑制するために存在します。volatileは典型的な処理系依存機能のうちのひとつであり、解釈は各コンパイラの実装に委ねられています。

C/C++volatileは、ときどきマルチスレッド間の簡易的な同期・通信用として使われていることがあります。たとえばサブスレッドでの処理完了を待機するために、グローバル変数などを用いて定義した処理完了フラグをメインスレッドにて監視・ポーリングする、といった状況です。

#include <cstdio>
#include <windows.h>
#include <process.h>
#include <conio.h>

volatile bool g_completed;

UINT CALLBACK MyThreadFunction(void*) {
    printf("Begin of sub-thread.\n");
    // ヘビーな処理を実行。
    for (int i = 0; i < 3; ++i) {
        printf("Executing task #[%d]...\n", i);
        ::Sleep(1000);
    }
    g_completed = true;
    printf("End of sub-thread.\n");
    return 0;
}

int main() {
    printf("Main thread waiting for completion of sub-thread...\n");
    auto hThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, MyThreadFunction, nullptr, 0, nullptr));
    while (!g_completed) {
        // メッセージ処理などを行ないながら待機。
        ::Sleep(1);
    }
    ::WaitForSingleObject(hThread, INFINITE);
    ::CloseHandle(hThread);
    hThread = nullptr;
    puts("Press any...");
    _getch();
}

※本来はC++11で標準化されたstd::threadを使ってもよいのですが、VC10.0 (VC++2010) などの古い処理系でもコンパイルできるよう、あえて古典的なCRTを使ってみました。

#include <cstdio>
#include <thread>
#include <chrono>

volatile bool g_completed;

void Sleep(int ms) {
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

void MyThreadFunction() {
    printf("Begin of sub-thread.\n");
    // ヘビーな処理を実行。
    for (int i = 0; i < 3; ++i) {
        printf("Executing task #[%d]...\n", i);
        ::Sleep(1000);
    }
    g_completed = true;
    printf("End of sub-thread.\n");
}

int main() {
    printf("Main thread waiting for completion of sub-thread...\n");
    std::thread thread(MyThreadFunction);
    while (!g_completed) {
        // メッセージ処理などを行ないながら待機。
        ::Sleep(1);
    }
    thread.join();
    puts("Finished.");
}

処理完了フラグはサブスレッドで書き換えますが、上記のようなケースにおいてフラグ変数をvolatileで修飾しない場合、コンパイル時の最適化により、フラグ変数へのアクセスを(メインスレッド-サブスレッド間で共有する)メインメモリではなく、(スレッドローカルな)レジスタにて実行するようなコードを出力してしまうことがあります。そうなると上記のポーリング用whileループの継続条件式は常に真となってしまい、メインスレッドはループを永遠に脱出できなくなります(無限ループ)。

ただし、こういったスレッド間の同期・通信用途はC/C++本来のvolatileの守備範囲ではなく、避けるべきです。そもそも、C++03規格およびそれ以前では、スレッドという概念そのものが標準化されていません。このようにvolatileがマルチスレッド同期に乱用されるようになった背景として、MSVCにおける独自の勝手な言語拡張*1があります。

VC++volatile拡張仕様では、なんとメモリバリアまで張ってくれるらしいです。つまり読み書き操作が暗黙的にアトミック処理になります。ただしx86/x64アーキテクチャARMアーキテクチャとでは既定の動作(コンパイラオプション)が異なるらしいので注意が必要です。移植性を考えると、VC++volatile拡張仕様に依存するべきではなく、メモリバリアはスレッドライブラリに用意されている同期オブジェクトを使って明示的に実装するべきです。

なお、一般的なアトミック操作の実現手段としては、C++11規格以降は基本的に標準ライブラリのstd::atomicを使うべきです。ただし、C++/CLI (/clr) では残念ながらstd::atomicが使えないそうで、代わりにWin32 Interlocked APIを直接使用するか、.NET標準クラスライブラリで用意されているアトミック処理用のSystem.Threading.Interlockedクラスなどを使います。C++/CLIにはC#lockステートメントに相当する組み込み構文は存在せず、またstd::mutexも使えないので、複雑な排他制御にはCRITICAL_SECTIONのようなWin32の同期オブジェクトを直接使用するか、System.Threading.MonitorSystem.Threading.SemaphoreSlimといった.NETの同期オブジェクトを直接使用するか、あるいはラッパークラスmsclr::lockを使用します*2処理系依存volatileを使うのは最後の手段にしましょう。

ちなみに前述の例は、WaitForSingleObject()の第2引数に1を指定して(タイムアウト時間1ミリ秒の待機とする)、ポーリングループ内で呼び出して戻り値をチェックする方法に変更すれば、volatileグローバルフラグ変数を取り除くことができます。

  while (::WaitForSingleObject(hThread, 1) == WAIT_TIMEOUT) {
    // メッセージ処理などを行ないながら待機。
  }

Java/C#

Java/C#volatileは、VC++拡張仕様のようなメモリバリアまではなされないものの、最適化を抑制する効果があります。C/C++と違い、処理系依存ではなくれっきとした言語仕様となっています。C#/Javaは当初からマルチスレッドを考慮した言語設計がなされており、volatile仕様に関してもマルチスレッドが考慮されているため、限定的ながらvolatileをスレッド間の同期・通信に使うこともできます。

Javavolatile変数に対するread/write自体はアトミックで、longdoubleにも指定することができます(ただし64bitのデータ型に対してアトミック命令が使われるとは限らない)。
C#でもvolatile変数に対するread/write自体はアトミックですが、longdoubleに指定することはできません(64bitのデータ型に対して、32bit環境ではアトミック命令が使えるとは限らないため)。

Java/C#volatileフィールドに対するインクリメント・デクリメントなどはアトミック操作にならないので、当然そういった用途にはjava.util.concurrent.atomicパッケージやSystem.Threading.Interlockedクラスなどを使うべきですが、一度だけ発生するフラグの単純な代入 (write) と参照 (read) による疑似的なシグナル用途など、volatileでもOKな場面もあります。運用制限を設けて賢く使いましょう。

*1:うわさによると、VC7.1 (VC++ .NET 2003) では、volatile関連で /Oa というさらにぶっ飛んだ最適化オプションがあったそうですが、このオプションはVC8.0 (VC++2005) で削除されたそうです。Visual Studio 2003 から 2015 の Visual C++ の新機能 | Microsoft Learn

*2:Windowsでの開発経験が浅く、Pthreadsや標準C++のスレッドライブラリしか使ったことがない人は勘違いしていることが非常に多いのですが、Win32のミューテックスオブジェクトや.NETのSystem.Threading.Mutexはプロセス間の排他制御に使います。スレッド間の排他制御に使うこともできますが、オーバーヘッドが大きいので基本的に使いません。

Visual StudioのビルドイベントでPowerShellを踏み台にしてC#を使う

Visual Studioでプロジェクトをビルドする際に、複雑な前処理・後処理を記述する場合、通例バッチコマンドによるカスタマイズをします。ただ、Windowsのコマンドは貧弱で、Unix/Linux環境のシェルなどとは比べ物になりません。
従来のバッチコマンドの代わりにPowerShellを使うのが近代的なWindowsプログラマーですが、個人的にはPowerShellの文法が好きではありません。言語機能も従来のバッチコマンドと比べると遥かに柔軟かつ高機能ですが、我々の大好きなC#言語と比べるとかなり書きづらいです。できればPowerShellよりもF#スクリプトC#スクリプトを使いたいのですが、これらはOS機能として統合・標準化されていないのが難点です。

そこで、PowerShellからC#コンパイラを使い、C#ソースコード文字列を渡してコンパイルし、C#で書かれたクラスをPowerShellから利用するという方法をとってみます。

param($rootDir)

$assemblies = (
#"System" # 不要。
"Microsoft.CSharp" # dynamic 型を使用するために必要。
)

$source = @"
using System;
using System.Runtime.CompilerServices;
public static class Test
{
  public static void CheckLambdaSpec()
  {
    var data = new[] { 1, 2, 3, 4, 5 };
    Action a = null;
    foreach (var x in data)
    {
      a += () => Console.WriteLine(x);
    }
    a();
  }

  public static void DoCallerInfoTestImpl([CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
  {
    Console.WriteLine(string.Format("Member name = \"{0}\"", memberName));
    Console.WriteLine(string.Format("Source file path = \"{0}\"", sourceFilePath));
    Console.WriteLine(string.Format("Source line number = {0}", sourceLineNumber));
  }

  public static void DoCallerInfoTest()
  {
    DoCallerInfoTestImpl();
  }

  public static void DoTest(string dir)
  {
    //string path = System.IO.Path.Combine(System.IO.Path.Combine(dir, @"Properties"), @"AssemblyInfo.cs");
    //using (System.IO.StreamReader reader = new System.IO.StreamReader(path))
    var path = System.IO.Path.Combine(dir, @"Properties", @"AssemblyInfo.cs");
    using (var reader = new System.IO.StreamReader(path))
    {
      string line;
      //Console.WriteLine(line?.Length.ToString() ?? "null");
      dynamic x = "hoge";
      Console.WriteLine(x.Length);
      while ((line = reader.ReadLine()) != null)
      {
        if (line.StartsWith("[assembly: AssemblyVersion("))
        {
          Console.WriteLine(line);
          break;
        }
      }
    }
  }
}
"@

try
{
  # ここでは、プロジェクトの出力先がカレント ディレクトリになる模様。
  #[System.Environment]::CurrentDirectory
  #$PSVersionTable
  $rootDir
  $rootDir.Length
  # コンパイル時に "%LocalAppData%/Temp/" にテンポラリ ソースファイル (*.cs) が生成される模様。
  Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $source -Language CSharp
  #Add-Type -TypeDefinition $source -Language CSharp
  [Test]::DoTest($rootDir)
  [Test]::DoCallerInfoTest()
  #[Test]::CheckLambdaSpec()
}
catch
{
  #Write-Error($_.Exception)
  Write-Error($_.Exception.Message)
  exit -1
}

上記PowerShellスクリプトをプロジェクトディレクトリに"test.ps1"として保存し、ビルド前あるいはビルド後イベントのコマンドラインとして、

powershell -ExecutionPolicy RemoteSigned -File "$(ProjectDir)test.ps1" "$(ProjectDir)\" ";exit $LASTEXITCODE"

を入力しておきます。
ここで、Visual Studio 環境変数PowerShell スクリプトコマンドライン引数として渡すとき、"$(ProjectDir)\" などとします。"$(ProjectDir)" ではダメなようです。末尾になぜか余計なダブルクォーテーションが入ります。

C#コンパイラのバージョン

前述のPowerShellからC#コンパイラを利用する手法自体に関してはWeb上のあちこちで言及されているのですが、そのC#コンパイラのバージョンに関してはほとんど言及がないようです。なので少し調べてみました。

前述の例はC# 5.0対応コンパイラが使えるという前提で記述しています。そもそもプリインストールされているPowerShellのバージョンもWindows OSによって異なるので、もしプロジェクトでPowerShellスクリプトを使う場合は最小バージョンに合わせて記述するか、開発者全員にPowerShellのバージョンアップを促しましょう。

Directory.Exists/File.Existsメソッドのタイムアウト時間

.NETのSystem.IO.Directory.Exists()メソッドは指定ディレクトリの存在有無をチェックするメソッドですが、タイムアウト時間が設けられています。ローカルドライブのディレクトリにアクセスする場合は、よほど低速なシステムか膨大なストレージでないかぎり、メソッドの実行は一瞬(数ミリ秒オーダー)で終わりますが、チェック対象がネットワーク共有フォルダーなどの場合、アクセスできないときはシステム規定のタイムアウト時間が経過するまで延々とリトライを続けます。この動作はSystem.IO.File.Exists()も同様です。なので普通、これらのI/O処理はメインスレッドで直接実行したりせず、サブスレッドに処理を逃がして非同期で実行するのがセオリーです。今どきのC#であればTPLやasync/awaitを使うのが常套手段でしょう。

Windows向けの.NET 4.x実装では、Directory.Exists()は内部でWin32 Shell APIPathFileExists()PathIsDirectory()を使っているものと思われます。確かネットワークアクセスのタイムアウト時間はWindows OSのシステム設定に依存するはずで、既定値は20秒だったと思います。また、タイムアウト時間はアプリケーション側では指定できないはずです。無効なネットワーク共有フォルダーにアクセスして、意図的にタイムアウトさせて時間を計測するために下記のC#スクリプトを試してみましたが、環境によって約20-30秒と開きがあるようです。ときどき40秒近くかかることもありました。ネットワーク共有フォルダーへのアクセスのタイムアウト時間に関しては、おそらく名前解決など、システムのネットワーク構成にも依存するものと思われます。

Action<string> checkDir = (path) => { var sw = new Stopwatch(); sw.Start(); Print(Directory.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkDir(@"\\1.2.3.4\hoge");

Action<string> checkFile = (path) => { var sw = new Stopwatch(); sw.Start(); Print(File.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkFile(@"\\1.2.3.4\hoge\fuga.txt");

C#スクリプト

C#コードをスクリプトとして対話的に実行できる「C# Interactive」はVisual Studio 2015 Update 1で追加された機能ですが、こういったAPIの挙動を調査するのに重宝しています。PrintメソッドはInteractiveScriptGlobalsクラスの静的メソッドのひとつですが、おそらくC# 6.0で追加されたusing static機能をC# Interactive内部で暗黙的に使い、using static InteractiveScriptGlobals;とすることでクラス名を省略できているものと思われます。using static自体はJava 5におけるimport staticの後追いのようなもので、不要な混乱を招きかねないため積極的に使うべき類の機能ではありませんが、こういったスクリプトコードに関してはC/C++Pythonなどのようにグローバルメソッドが簡潔に使えるということは重要ですね。

ちなみにC#スクリプトの拡張子は.csxで、C# Interactive外のVisual Studioコードエディターでもコード補完が効きますが、この拡張子のファイルをVisual Studio 2015 Update 3で編集していると、IDEが突然クラッシュするという残念な不具合があるようです。VS2017は試していません。

スレッドの強制終了について

I/O処理のタイムアウト時間が過ぎる前に処理を強制的に中断しようとして、ワーカースレッドをメインスレッドからSystem.Threading.Thread.Abortメソッドで強制終了しようなどとするのはご法度です。中断の結果として起こり得る未定義動作の危険性が説明されています。

Abortメソッドの危険レベルとしてはWin32 APITerminateThread関数と同程度で、よほどのことがないかぎり使うべきではありません。現に.NET Coreの仕様では、Thread.Abortメソッドは削除されているそうです。

JavaThreadクラスにもstopメソッドがありますが、やはり同様に非推奨となっています。