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

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

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

今更言うまでもありませんが、C/C++C#/Javaではキーワード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を使ってみました。

処理完了フラグはサブスレッドで書き換えますが、上記のようなケースにおいてフラグ変数を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が使えないそうで、代わりに.NET標準クラスライブラリで用意されている同期オブジェクトやアトミック処理用のSystem.Threading.Interlockedクラスなどを使います。処理系依存のvolatileを使うのは最後の手段にしましょう。ちなみに前述の例は、WaitForSingleObject()の第2引数に1を指定して(タイムアウト時間1ミリ秒の待機とする)、ポーリングループ内で呼び出して戻り値をチェックする方法に変更すれば、volatileグローバルフラグ変数を取り除くことができます。

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

C#/Java

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

C#/Javaのvolatileフィールドに対するインクリメント・デクリメントなどはアトミック操作にならないので、当然そういった用途にはInterlockedクラスなどを使うべきですが、単純な代入と参照による状態管理用途であればvolatileでもOKな場面もあります。運用制限を設けて賢く使いましょう。

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

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メソッドのタイムアウト時間

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

Windows向けの.NET 4.x実装では、Directory.Exists()は内部でWin32 Shell APIのPathFileExists()と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メソッドで強制終了しようなどとするのはご法度です。中断の結果として起こり得る未定義動作の危険性が説明されています。
Thread.Abort Method (System.Threading)
Thread.Abort Method (System.Threading) | Microsoft Docs

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

C# 6.0コンパイラーの興味深い挙動

C#言語はnullに関連する演算子が豊富です。

null合体演算子

null合体演算子は左側オペランドがnullでない場合は左側オペランドを返し、nullの場合は右側オペランドを返します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 2.0で追加されました。

x ?? alt

三項演算子で書くと以下のようになります。

x != null ? x : alt

null条件演算子

null条件演算子オペランドがnullでない場合はメンバーアクセス(.もしくは[])を実行します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 6.0で追加されました。

x?.SomeMethod();

if文で書くと以下のようになります。

if (x != null) { x.SomeMethod(); }

もしここでSomeMethod()が値型Tの戻り値を持つ場合、x?.SomeMethod()の評価結果はNull許容型T?となります。三項演算子で書くと以下のようになります。

x != null ? x.SomeMethod() : default(T?)

応用

これらの演算子を応用して、「引数がnullでない場合はToString()を呼び出し、nullの場合は代替のリテラル文字列"N/A"を返す」という処理をスマートに書いてみます。

// (1)
public static string ConvertToNAIfNull<T>(T x)
{
  return x?.ToString() ?? "N/A";
}

もしこれらの演算子を使わずに書くとすれば、以下のようになります。

// (2)
public static string ConvertToNAIfNull<T>(T x)
{
  return x != null ? x.ToString() : "N/A";
}

いずれにせよ1行で書けますが、(2)はxが2回出現して冗長なので、特にメソッドではなくインラインで書く際に不便そうですね。厳密に言うと、(1)と(2)は完全互換ではありませんが、通例ToString()は空文字列""を返却することはあってもnullを返却することはないので実用上は問題ないはずです。

ここで興味深いのが、(1)のジェネリクスには参照型やNull許容型だけでなく、通常の値型を渡しても実体化できるということです。csc 2015 (Visual C# 2015) でもgmcs 4.6.2でも動作します。

Console.WriteLine(ConvertToNAIfNull(new int?(10)));
Console.WriteLine(ConvertToNAIfNull(default(int?)));
Console.WriteLine(ConvertToNAIfNull(10));

値型に対する?.演算子呼び出しは無効なので、一見コンパイルエラーになってしまう気がしますが、(1)のジェネリクスC#コンパイラーが内部で以下のように展開するせいで、値型を渡してもコンパイルエラーにはならず実体化できる、というからくりになっているようです。

// (3)
public static string ConvertToNAIfNull<T>(T x)
{
  if (x != null) {
    var t = x.ToString();
    if (t != null) {
      return t;
    }
  }
  return "N/A";
}

値型に対してはx != nullコンパイルエラーにはならず常に真なので、(2)や(3)のジェネリクスに値型を渡せるのは当然です。いずれにせよ、ConvertToNAIfNull()に値型を渡すと冗長なメソッドになってしまいますが。

Windows 10でPhotoshop CS5が起動に失敗するときの対処

Windows 10 Anniversary Updateをインストールした後、Photoshop CS5が起動時にクラッシュするようになりました。管理者特権だと起動します。仕方ないのでWindows 7互換モードに設定すると起動するようになりました。

もともとCS5はWindows 10に正式対応していないし、無償サポートの期限も切れているので、まあ仕方ないといえば仕方ないんですが、別に新機能が欲しいわけでもないのにサブスクリプション契約を続けなければならないCCに移行したいとは思いません。CS5に限らず、この先もWin10に大型アップデートが来るたびに同じようなアプリケーション互換性のトラブルが発生しそうです。ただでさえカオスでキメラな設計のWin10に、これ以上余計な機能や仕様変更を加えてどうするつもりなのか。もはや余計な機能追加なんて誰も望んでいません。本来アプリケーションを安定して動かすのがOSの役目なのに、今のWindowsは出しゃばりすぎです。

余談ですがWin10のUpdateOrchestratorの挙動は極悪すぎます。勝手にWindows Updateを実行したうえ、ユーザーの許可なく勝手にスリープやハイバネーションを解除してPCを再起動するなんて、横暴にもほどがあります。どれだけユーザーをバカにしているんでしょうか。Creators Updateでは多少緩和されているようですが、それにしてもアップデートのたびにプライバシー設定をリセットするのは極めて卑劣なやり口です。あとせっかくカスタマイズしたWindowsエクスプローラーの設定をリセットするのもやめて欲しい。

Photoshopのレイヤー数をスクリプトで数える

本業の仕事が忙しかったこともあり、前回の記事から1年近く何も書けませんでした。すでにはてな記法を忘れつつありますが、ぼちぼち頑張ります。

Photoshop内部では、レイヤーを"art layer"、グループ(フォルダーアイコン)を"layer set"と呼んでいます。
今日はExtendScriptを使って、ドキュメント中のレイヤーとグループの数を自動的にカウントしてみます。

function getArtLayerCount(layerSet) {
    var na = layerSet.artLayers.length;
    for (var i = 0; i < layerSet.layerSets.length; ++i) {
        na += getArtLayerCount(layerSet.layerSets[i]);
    }
    return na;
}

function getLayerCountPair(layerSet) {
    var na = layerSet.artLayers.length;
    var ns = layerSet.layerSets.length;
    for (var i = 0; i < layerSet.layerSets.length; ++i) {
        var pair = getLayerCountPair(layerSet.layerSets[i]);
        na += pair[0];
        ns += pair[1];
    }
    return [na, ns];
}

//alert("Total art-layer count = " + getArtLayerCount(app.activeDocument));

var pair = getLayerCountPair(app.activeDocument);
alert("Total art-layer count = " + pair[0] + "\n" + "Total layer-set count = " + pair[1]);

app.activeDocumentを基点として、関数を再帰的に呼び出すことでトータルのレイヤー数およびグループ数を求めます。今回はレイヤー数とグループ数のペアを管理するのに配列を使ってみました。
ちなみにこのスクリプト、かなり負荷が高いので、100個くらいレイヤーがあるときにExtendScript Toolkit上でデバッグ実行するとめちゃくちゃ時間がかかります。デバッグが済んで不具合がないことが確認できたスクリプトはjsxファイルとして保存し、Photoshop本体の[ファイル]→[スクリプト]→[参照]でファイルを指定して実行しましょう。

Photoshop v7.0以降ではレイヤー数の上限は事実上ないようなのですが、SAI v1.xではレイヤー数上限が256 (255?) 枚までとなっているため、定期的にレイヤー数を確認して、不要なレイヤーを削除したり結合したりしておく必要があります。SAI v2では上限が8190枚となる予定だそうです。ぶっちゃけ64bitネイティブ移植して各種上限を撤廃するだけであればそんなに開発に時間がかかるとは思えないんですが、大幅な機能追加や仕様変更を伴っているせいで正式リリースがいつになることやら……気長に待つしかなさそうです。

Windows 10の画像ファイル右クリックメニューに「プレビュー」を追加

無償アップグレードの期限ギリギリになりましたが、ついにWindows 8.1からWindows 10にアップグレードしました。Windows 10は昨年のプレビュー版のときにDirectX 12 APIを少しテストした際に、全体的な完成度の低さが目についたため、それ以来一切触っていませんでしたが、2015年7月のリリースから1年を経てそれなりにこなれてきたようなので、まず念のためWindows 8.1のイメージバックアップを取ってからWindows 10に移行しました。しばらく使い込んでみて、ダメだったら8.1に戻す予定です。Windowsを使い続けるかぎり、いずれはWindows 10に誰しも移行せざるを得ませんが、自分の場合現時点でよく使うクリエイティブ系ツールやゲームが正常に動かなければ意味がありません。

Windowsフォトビューアー

本題に入りますが、Windows 7/8.1では、エクスプローラーで画像ファイルを右クリックした際に表示されるコンテキストメニューに、「プレビュー」という項目が含まれていました。このコマンドは、画像を「Windowsフォトビューアー」というデスクトップアプリケーションで開くことができるもので、拡張子すなわち既定の「開く」コマンドに関連付けたアプリケーションとは別に独立して使用できるコマンドです。「Windowsフォトビューアー」は、IrfanViewなどとは違ってフォルダー内の複数ショートカットファイル群を連続プレビューしたり、エクスプローラー上の並び順でファイルを閲覧することができたりといったメリットがありました。全体的な操作性や対応画像の種類などはIrfanViewに若干劣る面がありますが、カラープロファイル対応というのも何気にポイントが高いです。残念ながらWindows 10ではこのコマンドがなくなっているだけでなく、クリーンインストールした際には「Windowsフォトビューアー」自体が無効化されています。今回は幸いWin8.1からのアップグレードインストールだったので、コマンドを復帰するだけで済みました。復帰の手順は下記のようなサイトで紹介されています。

ascii.jp
https://moshimore.jp/knowledge/2015/08/21/windows_10_windows_photo_viewer_2/moshimore.jp
ryus.co.jp

毎回手作業で実行するのは面倒ですので、今回は「プレビュー」コマンド復帰の手順を一発で実行するためのレジストリファイルの内容を記載しておきます。UTF-16LE形式のテキストファイルで.reg拡張子を付けて保存してください。ただしご利用は自己責任でどうぞ。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\SystemFileAssociations\image\shell\openwpv]
@="プレビュー"

[HKEY_CLASSES_ROOT\SystemFileAssociations\image\shell\openwpv\command]
@="%SystemRoot%\\System32\\rundll32.exe \"%ProgramFiles%\\Windows Photo Viewer\\PhotoViewer.dll\", ImageView_Fullscreen %1"

[HKEY_CLASSES_ROOT\SystemFileAssociations\image\shell\openwpv\DropTarget]
"CLSID"="{FFE2A43C-56B9-4bf5-9A79-CC6D4285608A}"

Windows 10とSAI

今のところ、Win10でSAI v1.2.xの挙動が若干おかしいのが気になります。SAIはファイルI/Oなどの時間がかかる処理をサブスレッドではなくメインスレッド(UIスレッド)で実行しているレガシーアプリケーションなのですが、多数のレイヤーの回転・拡大縮小中とか、ファイルオープンダイアログでのサムネイル自動生成中とか、巨大な画像ファイルの読み書き中とか、UIスレッドが固まる状況でタスクバー部分が一定時間まったく見えなくなる現象が出ます。処理が完了すればタスクバーは復帰しますが、それまでは操作ができなくなります。v1.2.0/v1.2.5で確認しました。Win8.1だと同様にUIスレッドがビジー状態になったときにSAIのウィンドウがボケる(バイリニアフィルターがかかったようになる)現象が出ていましたが、タスクバーが見えなくなるというレベルのひどさではありませんでした。一方、Visual Studio 2012/2013/2015の場合、起動時など「応答なし」状態に陥ることがあるときでもタスクバーまで巻き込むようなことは一切ないので、おそらくSAI自身の設計に起因する固有問題なのだと思いますが、後継となるv2.0もいまだにプレビュー版のままなので、SAIを仕事で使っている人はWindows 10に移行するのは待ったほうがよいかもしれません。画像編集アプリなのに64bit対応が遅れているというネックもあるものの、クリスタでは使い勝手や書き味が違うため、泣く泣く32bit版のSAIを使い続けているという状況なのですが、早いところなんとかして欲しいです。
ちなみに、ちまたで噂されているペンタブレットの遅延ですが、今のところ特に書き味が悪くなったという感じはありません。検証したのはIntuos Pro M (PTH-651) + 6.3.16-2ドライバー環境ですが、それなりに高速なCPUを積んでいるので気にならないだけかも。
※2017-02-05追記:
Windows 10 Anniversary Update (version 1607, build 14393) をインストールして、グラフィックスドライバーをGeForce 378.49 for Win10 x64に更新したらSAIフリーズ時タスクバー問題が解消されたようです。もしかするとWindows 10 November 2015 Update (version 1511, build 10586) もしくは358.91ドライバーのどちらかに問題があったのかもしれません。ちなみにUpdate 1607を適用すると、コンテキストメニューからプレビューコマンドが再び削除されますので、再度登録作業が必要になります。
※2017-07-29追記:
Windows 10 Creators Update (version 1703, build 15063) をインストールすると、SAIフリーズ時にウィンドウがデスクトップ全体に引き延ばされる(タスクバー部分まで伸びる)現象が出るようになりました。グラフィックスドライバーはGeForce 378.49でも384.94でも発生します。ただ、タスクバーよりもZオーダーが下なので、タスクバーがまったく見えなくなったり、操作できなくなったりという実害はありません(単純に動きが気持ち悪いだけ)。

その他の落穂拾い

Windows 10では「Windowsフォトビューアー」の代わりにUWPの「フォト」アプリが搭載されていますが、前述のようなエクスプローラーとの統合はなく使いづらいです。「Windowsフォトビューアー」のサブセットでしかなく、IrfanViewと比較するとカラープロファイルに対応していることだけしかメリットがありません。また、Windows 10の電卓は相変わらず使いづらいストアアプリ(UWPアプリ)版のみしか搭載されていません。Windows 10 EnterpriseのLTSB版には従来のデスクトップアプリ版の電卓が「win32calc.exe」という名称で提供されているらしいのですが、Win10 Home/Proにもデスクトップアプリ版をぜひ搭載して欲しいです。