手作りで温もりのあるMSBuildプロジェクト

あけましておめでとうございます。

最小限のMSBuildプロジェクトファイルをテキストエディタで書いてみる回です。本当に知りたい方はこれを読むより公式ドキュメントのチュートリアルを読んでもらうのが一番いいです。おつかれさまでした。

MSBuildとは

The Microsoft Build Engine is a platform for building applications. This engine, which is also known as MSBuild, provides an XML schema for a project file that controls how the build platform processes and builds software. Visual Studio uses MSBuild, but it doesn't depend on Visual Studio. By invoking msbuild.exe on your project or solution file, you can orchestrate and build products in environments where Visual Studio isn't installed.

MSBuild - Visual Studio | Microsoft Docs

ということで、アプリケーションをビルドするためのMicrosoft製のエンジンです。.NET Framework 2.0から同梱されていて、それまで使われていたNMakeの.NET Framework版みたいなもんです*1

.NET Framework 1系までは、Visual Studioでのビルドは、IDE組み込みのdevenv.exeがビルドしている一方、コマンドラインからのビルドはcsc.exeやvbc.exeを直接使っていました。コンパイラオプションを指定するのが大変だったり、ビルド以外の処理が複雑でした。これらをまとめてスクリプト化したのがMSBuild及びプロジェクトファイルです。

例えば、Visual Studioで作ったプロジェクトをJenkinsなどのCIツールでもビルドしたいとき、そのままプロジェクトファイル(.csprojとか)を入力にしてMSBuild.exeを実行すれば同様の成果物が生成されます。便利ですね。

Why 手作り

前述のとおり、通常?はVisual Studio様がご創造なされますが、細々とした設定をしたいときや、ビルド環境によって切り替えたいとき、またコンソール実行で設定を渡したいときなど、けっこう触る機会があります。少しでも手を出そうとすると、結局MSBuildが何をするかを把握しておかないといけません。こういう場合、往々にして既存のプロジェクトファイルをいじるところから始めてしまって結局無為な時間を過ごしてしまいがちです。でした。私が。

主にプロパティ、項目、ターゲット、タスクという独特な概念を理解する必要があります。ドキュメントを読んで理解できるならそれで十分ですが、実際に動かすのが一番だと思います。のでやってみた次第です。

Let's 手作り

あえてテキストファイルでプロジェクトファイルを作ってみます。MSBuildでビルドする最小限からはじめて、少し遊んでみます。OSはmac OS Mojave 10.14、MSBuildは15.6.0.0ですが、超基本的なことしかやってないので、各自の環境でも再現できると思います。

最小構成でMSBuild

最小限のC#のコードを書きます。ファイル名を helloworld.cs とします。

using System;

class HelloWorld  
{  
    static void Main()  
    {
        Console.WriteLine("Hello, world!");  
    }
}

最小限のプロジェクトファイルを書きます。ファイル名は Helloworld.proj とします。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <Compile Include="helloworld.cs" />
  </ItemGroup>
  <Target Name="Build">
    <Csc Sources="@(Compile)"/>
  </Target>
</Project>

XMLで記述します。根本は <Project> 要素です。 <ItemGroup> 要素は、MSBuildへの入力となるファイルを指定します。ここでは helloworld.cs というファイルを、Compileという種類のItem(=項目)にIncludeしています。 <Target> 要素は、MSBuildが順次実行する処理(タスク)をまとめたものです。Targetの名前をBuildとしています。BuildというTargetを実行すると、配下のタスクが実行されます。ここでは、 Cscタスクに対してCompileというItemを渡しており、ItemGroupで指定したhelloworld.csのみが渡されます。Cscタスクはcsc.exeをラップしており、実際のビルド処理はこのタスクが行います。

この状態で、MSBuildを実行します。 -t スイッチでTargetを指定します。

$ msbuild Helloworld.csproj -t:Build
Microsoft (R) Build Engine version 15.6.0.0 (xplat-master/ca830585 Sun Mar 25 19:24:09 EDT 2018) for Mono
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 1/5/2019 9:50:17 PM.
Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" on node 1 (Build target(s)).
Build:
  /Library/Frameworks/Mono.framework/Versions/5.10.1/lib/mono/msbuild/15.0/bin/Roslyn/csc.exe /out:helloworld.exe helloworld.cs
Done Building Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" (Build target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.01

これでhelloworld.exeが出力され、実行できます。これで最低限のビルドができます。Visual Studioが自動生成するプロジェクトファイルに比べるとかなりシンプルです。

ビルドプロパティの追加

最低限のビルドはできるようになったので、もう少し細かい制御をしてみます。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>  
   <AssemblyName>MSBuildSample</AssemblyName>  
    <OutputPath>Bin\</OutputPath>  
  </PropertyGroup>  
  <ItemGroup>
    <Compile Include="helloworld.cs" />
  </ItemGroup>
  <Target Name="Build">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>
</Project>

<PropertyGroup> 要素は各種のプロパティを設定します。ここでは、出力するアセンブリ名と出力先のディレクトリを指定しています。合わせてBuildターゲットにおいて、Cscタスクの前に出力ディレクトリの作成と、Cscタスクにおいて出力アセンブリ名を指定しています。

この状態でMSBuildを実行すると、 Binディレクトリが作成され、その配下にMSBuildSample.exeが作成されます。

ビルドターゲットの追加

Build以外に、CleanとRebuildというターゲットを追加します。また、プロジェクトのデフォルトのターゲットを指定します。

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <AssemblyName>MSBuildSample</AssemblyName>  
    <OutputPath>Bin\</OutputPath>  
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="helloworld.cs" />  
  </ItemGroup>
  <Target Name="Build">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>
  <Target Name="Clean" >  
    <Delete Files="$(OutputPath)$(AssemblyName).exe" />  
  </Target>
  <Target Name="Rebuild" DependsOnTargets="Clean;Build" />  
</Project>

Cleanターゲットでは、Deleteタスクを用い、Buildターゲットで生成したアセンブリを削除します。RebuildターゲットはCleanとBuildのターゲットをこの順序で実行します。

この状態でCleanターゲットを実行してみます。

$ msbuild Helloworld.csproj -t:Clean
Microsoft (R) Build Engine version 15.6.0.0 (xplat-master/ca830585 Sun Mar 25 19:24:09 EDT 2018) for Mono
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 1/5/2019 10:21:19 PM.
Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" on node 1 (Clean target(s)).
Clean:
  Deleting file "Bin/MSBuildSample.exe".
Done Building Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" (Clean target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.42

Bin下のMSBuildSample.exeが削除されています。また、MSBuildを-tスイッチなしで実行すると再度MSBuildSample.exeが作成されます。

インクリメンタルビルド

ターゲットが依存しているファイルのタイムスタンプを見て、変更があった場合にのみビルドします。BuildターゲットにInputsとOutputsを指定します。

  <Target Name="Build" Inputs="@(Compile)" Outputs="$(OutputPath)$(AssemblyName).exe">>
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>

この状態で msbuild -v:d を実行します。ビルドの詳細を出力するスイッチをしています。

Skipping target "Build" because all output files are up-to-date with respect to the input files.
Input files: helloworld.cs
Output files: Bin/MSBuildSample.exe

入力ファイルhelloworld.csに変更がないので、Buildターゲットがスキップされています。

ビルドしない

ここまでくるとわかるかと思いますが、上記のプロジェクトにおいて、実際にビルドを実行しているのは結局Cscタスクであり、その前後は付随する処理でしかありません。つまり、MSBuildが行うのはビルドだけでなくその前後を含めたプロセスといってよさそうです。ビルドしないMSBuildプロジェクトもありえます。

例として、まったく意味のないプロジェクトを作ってみます。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="Greeting">
    <Message Text="Hello" Importance="low" />
    <Message Text="World" />
    <Message Text="!!!!!" Importance="high" />
  </Target>
  <Target Name="Copy">
    <Touch Files="File1.cs;File2.cs" AlwaysCreate="true" />
    <Copy SourceFiles="File1.cs" DestinationFolder="Bin\" />
  </Target>
</Project>

GreetingターゲットとCopyターゲットの2つがあります。まずGreetingターゲットは、Messageタスクを使って、文字列を出力するだけです。実行してみます。

$ msbuild NoBuild.csproj -t:Greeting -v:d
(略)
Target "Greeting" in project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (entry point):
Using "Message" task from assembly "Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a".
Task "Message"
  Hello
Done executing task "Message".
Task "Message"
  World
Done executing task "Message".
Task "Message"
  !!!!!
Done executing task "Message".
Done building target "Greeting" in project "NoBuild.csproj".
Done Building Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (Greeting target(s)).
(略)

「Hello」「World」「!!!!!」が出力されています。HelloはImportanceがlowなので、 -v:dオプションがないと出力されません。

次に、Copyターゲットを実行してみます。Touchタスクを使ってFile1.csとFile2.csを作成した上で、CopyタスクでFile1.csをBin配下にコピーします。

$ msbuild NoBuild.csproj -t:Copy
(略)
Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" on node 1 (Copy target(s)).
Copy:
  Touching "File1.cs".
  Touching "File2.cs".
  Copying file from "File1.cs" to "Bin/File1.cs".
Done Building Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (COpy target(s)).
(略)

まとめ

MSBuildプロジェクトファイルを0から手で書いてみました。ほとんど公式チュートリアルの丸パクリです。本年度のブログのハードルを下げていく方針なので大成功です。

Visual Studioを使って普通に開発を進めていくと気づいたらプロジェクトファイルが巨大になっていて面くらいがちです。でもやっていることは単純なことがわかります。基本的に手で書くものではないですが、上記内容くらいは知っておいて損はない気がします。他にもいろいろなタスクがあったり、条件指定ができたり、細かい制御ができます。そのあたりはぜひリファレンスを参照ください。

*1:MSBuildの開発コード名は「XMake」でした

2018年の振り返り

帰省中です。雪だし寒いしもうダメです。

2018年の振り返り

技術ブログ

このブログだと、2018年は技術関係のエントリが5本だけでした。残念の極みです。2019年は5000本くらい書きたいです。また、勤務先の公式ブログに記事を書きました*1。はてブのホットエントリになった、ということを聞きました。そこそこ褒めてもらえたので、たぶんいいことなのだと思います。

勉強会

技術勉強会は発表側にならないと何も学ばず時間を浪費してしまう自覚があるので、だいたい発表前提で参加します。2018年は勉強会自体にほとんど参加しませんでした。ただ、こちらも初めて勤務先オフィシャルな場で社員として登壇しました*2。中身は課題だらけ過ぎて忘れたいです。

技術書典

ノリで応募して勉強しながら書きすすめましたが、結果的に楽しく充実した体験ができました。BOOTHにも出品しており、現時点で126冊のご購入をいただいております。ありがとうございます。次回は春頃に開催されるはずです。一般参加よりもサークル参加を強くおすすめします*3

kyabatalian.hatenablog.com

扁桃切除手術

扁桃切除の手術をうけるため、10日間の入院をしました。以降、扁桃炎にはなっていないのですが、手術による改善はまあそれほど体感できていません。食べ物が飲み込みやすくなったような気がします。入院中にキングダムを読破し、中華統一への思いが高まりました。

kyabatalian.hatenablog.com

旅行

宿泊を伴う観光でいうと、京都と石川に行きました。京都へはGWと夏(祇園祭の直前頃)の2回行きました。常に最高です。石川は和倉温泉に行きました。技術書典の前日かつ台風が来ていて怖かったです。こちらも最高でした。2019年は海外に行きたいです。

kyabatalian.hatenablog.com

お笑い

趣味です。2018年は主にド地下のライブに足繁く通っていたのですが、2019年はわりとメジャーどころも観に行くようになりました。推しの単独ライブ、最高でした*4。直接みにいったライブに限らず、キングオブコントやM-1グランプリも楽しみました。2018年の私的ベストは、5月にルミネでみた、かまいたちの漫才でした。異次元の面白さでした。

kyabatalian.hatenablog.com

人生

このブログを書くにあたり、1年間のツイートをさかのぼってみたところ、前半はかなりつらそうでした。お仕事がしんとかったのか、何かあったのだと思うのですが、まったく思い出せないので、人生そんなものなのかもしれません。後半は今後の人生について考える機運が高まり、それはそれでハードでした。2019年は行動にうつしていきたいです。

2019年の抱負

技術をちゃんとやります。自分がもし第三者だったらいまの自分をみて「いいから黙ってコード書けよ」と言いたくなりそうなので黙ってコード書きます。

そのためには健康第一です。来年もよろしくおねがいします。良いお年を。

拡張統計情報とテーブル結合

PostgreSQL Advent Calendar 2018、23日目の記事です。

今更感満載ですが、PostgreSQL 10で導入された拡張統計情報について、直感と異なる挙動だったので調査してみました。小ネタですみません。

TL;DR

  • PostgresSQLのオプティマイザは、カラム間の関数従属性を考慮しない
  • 拡張統計情報を使えば、テーブル探索で関数従属性を考慮できる
  • テーブル結合では考慮されない

行数推定の課題

PostgreSQLのオプティマイザはコストベースであり、アクセスパスツリーを生成して最もコストの低いパスを選択します。

このコスト計算において、条件句が複雑な場合に、実体と沿わないコストを算出してしまう場合があります。具体的には、条件句に AND で複数の条件が指定され、かつ両条件に指定されたカラム間に関数従属性がある場合、実際の行数よりも推定行数が少なく見積もられます。

この時点で???となった方は、ググるか、恐縮ながら以下をさらっとみてもらえるといいかもしれません。

PostgreSQL:行数推定を読み解く/row-estimation - Speaker Deck

拡張統計情報

任意のテーブルがもつカラム間の相関を取得し、オプティマイザに使わせる事ができる機能です。PostgreSQLのバージョン10で導入されました。

以下ではまず、この機能がどういうものか確認してみます。公式ドキュメントの例を使います。

-- テスト用のテーブルt1を作成する
CREATE TABLE t1 (a int, b int);

-- t1にデータを挿入する
INSERT INTO t1
  SELECT i/100, i/500
  FROM generate_series(1,1000000) s(i);

こうすることにより、t1のデータは以下のようになります。

i a b
1 0 0
2 0 0
3 0 0
99 0 0
100 1 0
101 1 0
102 1 0
199 1 0
200 2 0
201 2 0
499 4 0
500 5 1
501 5 1
999 9 1
1000 10 2
1001 10 2

つまり、というかSQLのとおりなんですが、aは100ごとに、bは500ごとに値がインクリメントされます。例えばaが12とわかればbは2と確定しますし、逆にbが2とわかればaは10以上20未満であるとわかります。このとき、カラムaとカラムbは相互に関数従属であるといいます。

この状態で、カラムa、bの両方に対する条件句をもつSQLの実行計画をみてみます。

-- 統計情報を収集する
ANALYZE t1;

-- 実行計画を取得する
EXPLAIN ANALYZE SELECT * FROM t1 WHERE (a = 1) AND (b = 0);
Seq Scan on t1  (cost=0.00..19425.00 rows=1 width=8) (actual time=0.034..113.435 rows=100 loops=1)
  Filter: ((a = 1) AND (b = 0))
  Rows Removed by Filter: 999900
Planning time: 0.155 ms
Execution time: 113.467 ms

t1に対するSeq Scanの実際の結果は100行にもかかわらず、推定行数が1行になっています。これは、カラムaとbの間の関数従属性が考慮されていないためです。つまり、各条件句を独立とみなし、それぞれ別のSQLで取得した結果の和集合としています。

この問題に対する解決策として、拡張統計情報があります。ためしてみます。

-- 拡張統計情報を作成する
CREATE STATISTICS s1 (dependencies) ON a, b FROM t1;

-- 統計情報を収集する
ANALYZE t1;

-- 実行計画を取得する
EXPLAIN ANALYZE SELECT * FROM t1 WHERE (a = 1) AND (b = 0);
Seq Scan on t1  (cost=0.00..19425.00 rows=98 width=8) (actual time=0.020..89.729 rows=100 loops=1)
  Filter: ((a = 1) AND (b = 0))
  Rows Removed by Filter: 999900
Planning time: 0.083 ms
Execution time: 89.750 ms

今度はカラム間の関数従属性が考慮され、行数は正確に見積もられています。

テーブル結合

本題、テーブル結合です。t1と全く同様の方法で、t2とt3という2つのテーブルを作り、これらをクロス結合して実行計画をみてみます。

CREATE TABLE t2 (a2 int, b2 int);
CREATE TABLE t3 (a3 int, b3 int);

INSERT INTO t2 SELECT i/100, i/500 FROM generate_series(1,1000000) s(i);
INSERT INTO t3 SELECT i/100, i/500 FROM generate_series(1,1000000) s(i);

単一条件の場合

まず、t2に対する条件句が単一の場合の実行計画を見てみます。

EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = t3.b3);
Hash Join  (cost=30832.00..52282.01 rows=48922 width=16) (actual time=614.425..622.979 rows=49900 loops=1)
  Hash Cond: (t2.b2 = t3.b3)
  ->  Seq Scan on t2  (cost=0.00..16925.00 rows=98 width=8) (actual time=0.030..85.213 rows=100 loops=1)
        Filter: (a2 = 1)
        Rows Removed by Filter: 999900
  ->  Hash  (cost=14425.00..14425.00 rows=1000000 width=8) (actual time=475.964..475.965 rows=1000000 loops=1)
        Buckets: 131072  Batches: 16  Memory Usage: 3334kB
        ->  Seq Scan on t3  (cost=0.00..14425.00 rows=1000000 width=8) (actual time=0.024..146.287 rows=1000000 loops=1)
Planning time: 0.331 ms
Execution time: 626.888 ms

t2に対するSeq Scanの推定行数が98となっており、実際には100なので、乖離はありません。結合条件としてはHash Joinが選択されています。

複合条件の場合(拡張統計情報なし)

テーブルt2のカラムa2、b2それぞれに対して条件句を指定します。関数従属性により、実行計画が狂ってしまうのを確認します。

EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = 0) AND (t2.b2 = t3.b3);
Nested Loop  (cost=0.00..36354.86 rows=486 width=16) (actual time=0.056..8576.923 rows=49900 loops=1)
  ->  Seq Scan on t2  (cost=0.00..19425.00 rows=1 width=8) (actual time=0.032..79.398 rows=100 loops=1)
        Filter: ((a2 = 1) AND (b2 = 0))
        Rows Removed by Filter: 999900
  ->  Seq Scan on t3  (cost=0.00..16925.00 rows=486 width=8) (actual time=0.007..84.883 rows=499 loops=100)
        Filter: (b3 = 0)
        Rows Removed by Filter: 999501
Planning time: 0.113 ms
Execution time: 8580.450 ms

前項のt1と同じく、t2は推定行数が大きくずれ、Seq Scanとなっています。結合に関しては、t2が1行となる予測なので、Nested Loopが選択されています。

複合条件の場合(拡張統計情報あり)

テーブルt2のカラムa2、b2に対して、拡張統計情報を作成します。

CREATE STATISTICS s2 (dependencies) ON a2, b2 FROM t2;
ANALYZE t2;
EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = 0) AND (t2.b2 = t3.b3);
Nested Loop  (cost=0.00..36945.60 rows=47628 width=16) (actual time=0.045..231.646 rows=49900 loops=1)
  ->  Seq Scan on t3  (cost=0.00..16925.00 rows=486 width=8) (actual time=0.019..119.208 rows=499 loops=1)
        Filter: (b3 = 0)
        Rows Removed by Filter: 999501
  ->  Materialize  (cost=0.00..19425.49 rows=98 width=8) (actual time=0.000..0.205 rows=100 loops=499)
        ->  Seq Scan on t2  (cost=0.00..19425.00 rows=98 width=8) (actual time=0.022..97.727 rows=100 loops=1)
              Filter: ((a2 = 1) AND (b2 = 0))
              Rows Removed by Filter: 999900
Planning time: 0.194 ms
Execution time: 235.587 ms

実行時間が8580msから246msに改善しました。実行計画の中身をみてみると、t2の推定行数が98になっており、実行結果の100行と僅差になっています。それにより、Materializeされるようになりました。Materializeは、t2をSeq Scanした結果をメモリにのせます。これにより、t3とのNested Loopにおいてt2の再スキャンを高速化します。

考察

拡張統計情報によって見事に実行速度が改善したわけですが、Nested Loopではなく単一条件の場合と同じくHash Joinが選択されてもよさそうです。なぜNested Loopのままなのか、PostgreSQLのコードを見てみます。

拡張統計情報を参照するのは clauselist_selectivity() の以下の箇所です。該当コードはここ

 /*
    * Determine if these clauses reference a single relation.  If so, and if
    * it has extended statistics, try to apply those.
    */
    rel = find_single_rel_for_clauses(root, clauses);
    if (rel && rel->rtekind == RTE_RELATION && rel->statlist != NIL)
    {
        /*
        * Perform selectivity estimations on any clauses found applicable by
        * dependencies_clauselist_selectivity.  'estimatedclauses' will be
        * filled with the 0-based list positions of clauses used that way, so
        * that we can ignore them below.
        */
        s1 *= dependencies_clauselist_selectivity(root, clauses, varRelid,
                                                  jointype, sjinfo, rel,
                                                  &estimatedclauses);

        /*
        * This would be the place to apply any other types of extended
        * statistics selectivity estimations for remaining clauses.
        */
    }

    /*
    * Apply normal selectivity estimates for remaining clauses. We'll be
    * careful to skip any clauses which were already estimated above.

スキャン方法の選択時、 src/backend/optimizer/path/costsize.c がこのコードを使っています。

void
set_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel)
{
    double     nrows;

    /* Should only be applied to base relations */
    Assert(rel->relid > 0);

    nrows = rel->tuples *
        clauselist_selectivity(root,
                               rel->baserestrictinfo,
                               0,
                               JOIN_INNER,
                               NULL);

一方、結合条件について。PostgreSQLのオプティマイザはデフォルトでNested Loopのパスを作り、そのあとにMerge Join、Hash Joinとそれぞれを検討して、そっちのほうが速そうならそっちに置き換えます。 このへんでHash Joinを選択するか判断しているのですが、この段階で上記コードを参照していません。つまり、結合条件の選択に、拡張統計情報はつかわれません。

改めて問題

何が困るかというと、まあそのままなのですが、「巨大なテーブル同士をNested Loopでジョインしてしまう」ことです。これについては、バージョン10以前と同様にpg_hint_planをつかって結合ルールを手動制御せざるを得なさそうです。

そんな人はいないと思いますが、「関数従属な複数カラムに対する条件句」と「テーブル結合」の両方を持つSQLがたくさんあり、pg_hint_planでがんじがらめになっているサービスを運用しており、拡張統計情報をメシア視している方がもし仮に存在するなら、要注意です。

まとめ

普通に結合条件でも考慮されてほしいと思いました。拡張統計情報という壮大な名前なので、今後さらに賢くなっていくことが期待できます。と思っていたらバージョン11では特にアップデートがなかったです。悲しいです。以降、「文句言ってないでOSSなんだから自分でなおせよ」禁止。

参考

ポストモーテムにおける根本原因分析

SRE Advent Calendar 2018、僭越ながら3日目を担当させていただきます、@kabaomeです。

勢いでカレンダーに登録しましたが、私は明示的にSREというポジションではなく、一アプリケーションエンジニアです。 半年くらい前にSRE本*1を読み、「こういうのやっていくゾ☆」と思い立って、社内で勝手にいろいろやっています。いまのところ怒られていません。

ポストモーテム、その中でもっとも重要であり難しい(と思っている)根本原因分析について、個人的見解を書いてみます。

TL;DR

  • ポストモーテムやその根幹である根本原因分析の情報あんまない
  • 根本原因分析の結論は「客観的」「単純」「制御可能」であるべき
  • 根本原因分析はスキルだと思います、がんばっていきましょう

ポストモーテム

まずはポストモーテムについて簡単にご紹介します。 SREの皆様におかれましては常識だと思いますので、読み飛ばしてください。

ポストモーテムとは、もともとは医学の世界で、「検死」を意味する英単語です。 検死とは、「死体について医学的に検査し、死因や死亡の状況について判断を下す」仕事を指します*2。翻って、プロジェクトマネジメントの世界においても、プロジェクトの結果、主に失敗ケースに対して振り返りを実施し、その原因や再発防止策を策定する営みとして広く知られています。

私を含むソフトウェアエンジニアに広くこの言葉が知られたのは、やはりGoogleのSREによる取り組みであり、それを流布したSRE本にあると思います。以後、ポストモーテムを(本来の用語ではなく狭義の)言葉として用います。

ポストモーテムとよく似た文脈で、いわゆる「インシデント報告書」がよく出てきます。 こちらも一般的な定義はありませんが、「サービスに障害が発生し、顧客が不利益を被った場合に、顧客に対する説明として提出する文書」を指します。インシデント報告書とポストモーテムはその目的・存在意義から明確に異なっています。

目的 想定読者
インシデント報告書 インシデントの報告・情報共有・謝罪 顧客、上司などのレポートライン
ポストモーテム インシデントからの学び・サービス改善 開発者

ポストモーテムにおいて、インシデントは学びの対象であり、サービスの信頼性を向上させるための材料です。その視点で振り返り、アクションプランをたて、またチーム内に共有します。

ポストモーテムそのものについては、SRE本及びWorkbookを読んでいただければ良いのですが、いかんせんGoogle以外での実例を聞いたことがほとんどありません。SRE的改善!自動化!みたいなのはカッコよくて対外的にもオープンにしやすい一方で、ポストモーテムはインシデントに関する情報でセンシティブなため、あまり外に出さない力学が働くのではないかと邪推しています。

そこで今回は、SRE本のポストモーテム章をさらに焦点を絞って、「根本原因分析」について、これまでやってきた経験をもとに書いてみます。

根本原因分析

Root cause analysis (RCA) is a method of problem solving used for identifying the root causes of faults or problems.

この定義からわかるように、根本原因分析はあくまで「問題の解決」を目的としており、そのために原因を特定する手法です。

ドラッカーが再三説いたように、ソフトウェアの運用にとどまらずビジネスなど人間の知的活動のあらゆる局面において、物事を進める上で最も重要視されるべきものの1つが「正しい問いをすること」です。人間は「いかに正解を出すか」に陥りがちですが、真に重要なのはその前段、解こうとしている問題が正しいかに頭をつかうべきです。間違った問いに対する正しい答えが最悪、というのはよく言われる話です。

インシデントの分析においても同様で、間違った原因に対する正しいアクションは最悪です。余計な運用手順を増やしたり、象徴的にはドキュメントが無限に膨れ上がっていきます。そこでRCAを行うわけですが、これが激ムズです。

根本原因に到達するための問い

よりよい学び、ひいてはサービスの信頼性向上のために、根本原因分析の結論に対する心構えを、問いの形で3つ挙げてみます。それは、「客観的か?」「単純か?」「制御可能か?」です。

以下では例としてデプロイ作業時のインシデントに関するRCAの結論に対し、何が良くないのか、どうすればいいのかを考えてみます。

  • 作業:あるWebサービスに新機能を追加してデプロイする
  • 事故:デプロイ手順にある、ロードバランサを切り離す作業を実施しなかった
  • 結果:デプロイ中にアクセスした複数のユーザがエラー画面を経験した

客観的か?

事実を客観的に書くように努めるべきです。 人間が絡むあらゆる営みで、主観を完全に排除することは不可能です。先のインシデント例において、客観性を欠いた原因として、例えば以下です。

  • 原因:デプロイ手順をしっかり確認していなかった

これは、デプロイ作業を行った担当者が自らRCAとポストモーテムの記述を行ったときに出てきやすい結論です。デプロイ手順を確認していなかったかどうかは程度問題であり担当者の主観によります。「しっかり」とか「確実に」とか、程度を示す言葉が書かれていれば要注意です。このような原因をおいてしまうと、「気をつける」みたいな主観ありあり、実効性のないアクションに結びつきがちです。

往々にしてポストモーテムを書くのはそのインシデントに詳しい人間が担当者になりやすく、インシデントとの距離が近ければ近いほど、主観をもって分析をしてしまいます。客観的な分析のための方法として、客観性を担保する方法として確実に有効なのは、第三者の利用です。例えば以下のようなものが考えられます。

  • インシデントとは直接関係のない人間が関係者にヒアリングして記述する
  • 主観的なワード(「しっかり」「確実に」など)を排除する

重要なのは、前者では「客観視できる人間の視点を入れる」、後者では「機械的に修正点を見つける」としているように、「インシデントを書く人間に客観性を求める」という精神論ではなく、分析自体も客観性のあるアプローチを取ることです。人間を変えるよりも環境を変えるほうがやりやすいということを前提とします。

単純か?

なるべく単純に、平易に、わかりやすく書くように努めるべきです。 先の例でいうと、以下のように複数の原因が複雑に記述されると、単純さを欠いています。

  • 原因1:デプロイ手順が複雑すぎた
  • 原因2:デプロイ手順を確認することの重要性が認識されていなかった
    • 新入社員研修のカリキュラムに含まれていなかった
    • 先輩社員によるデプロイ手順のレビューがなされていなかった
  • 原因3:スケジュールの都合上、余裕がなかった

複雑な原因が良くない理由として、3つ挙げて紹介します。

問題を正確に捉えきれていない

起きた事象が明確ならば、その原因も明確に存在するはずです。もちろん複数の要因が絡み合ってひとつのインシデントを形成する場合もありますが、ほとんどの場合は表面的な分析にとどまっており、深掘りが足りないケースが多そうです。起きた事象の記述自体が複雑ならば、まず事実確認まで立ち返ったほうがいいかもしれません。

アクションの実効性を落とす

複雑な原因から導き出されるアクションは、同様に煩雑になってしまいがちです。例えば上記例だと、原因が3つ挙げられているため、それぞれを解決するアクションを検討します。本来は「デプロイ手順を自動化する」というアクションが最も解決に近かったのに、「ドキュメントを整備する」「新人研修カリキュラムをみなおす」など、本質的でないアクションに陥り、運用コストや整備の工数が無駄に発生することになりかねません。

読みづらい

他のドキュメントと同じくポストモーテムもまた人間が読むことを想定した文書です。読みづらい文書を読みたい人間はいません。

原因をシンプルに落とし込むための手法としては、まずは愚直に「3行以内にまとめる」「1つだけにする」といった方法が考えられますが、実際に起きた問題が本当に複雑な原因を持っているケースも多々あり、本質的ではありません。

そこで掘り下げる方向での分析が必要ですが、手法は様々あります。有名どころでひとつ紹介しておくと、「なぜなぜ分析」があります。これは、トヨタ生産方式を構成する手段の一つです。方法はとてもシンプルで、「なぜ」を問い、それに答え、そうなった理由についてまた「なぜ」を問う、ということを繰り返していきます。

制御可能か?

コードやマネジメントのレベルでうまく制御できていれば解決していたのか、という視点です。 例えば前述のインシデント例で、制御不可能な原因は以下です。

  • 原因:ロードバランサが自動で切り離れなかった

振り返りの常ですが、結果論に陥りがちです。そうなっていれば起きなかったこと、を挙げること自体は間違いではないのですが、起こったあとだから言えることは当時知り得ないことなので、原因とは言えません。この例でいうと、自動化されていなかったことが原因として起きる問題というのは本来ありえないはずで、人の手で行ったことにより生じた不具合が原因としてあり、それに対するアクションとして「自動化」が挙がるのが正しい姿です。

制御可能性に関するもうひとつの大きな視点として、「人間はそのとき最善の判断をした」ことを前提とすることも重要です。SRE本にはそのように書いてあるのですが、「Google様は神エンジニア集団だからその前提でいけるけどウチは無理」と解釈しがちです。これは不遇な誤解だと思っていて、人間に原因を求めないというのは組織やサービス改善の重要な指針になりますし、そもそも人間は改善できないです。人間がミスったと思うなら、なぜその人間はミスったのか、深掘りすべきです。

まとめ

根本原因分析は、多くの場合に正解がなく、主観や複雑さとの闘いでもあり、非常に難しいです。

あらゆるロールの人間がインシデントに向き合い、その貴重な機会から学びや改善を得るために、根本原因分析をがんばっていくといいと思います。

偉そうに書きながら私もまったくできていません。しかし、思考の訓練をしながらそのスキルを上げていくことができると思っています。

他の組織での話もぜひ聞いてみたいので、ご興味のある方はコメント欄か@kabaomeまでご連絡ください。やっていきましょう。

参考

*1:https://landing.google.com/sre/books/

*2:法律上の定義はないため、参考文献より引用

扁桃切除手術の記録

扁桃切除手術*1を受けました。
退院後、周囲の人がいろいろと聞いてくれたり、扁桃切除を検討しているので教えてほしい、と聞かれることが幾度かあったため、ここに記しておこうと思いました。

基礎知識

どういう手術なのかを簡単に紹介します。

扁桃とは

まず扁桃という器官は、喉にあるリンパ器官です。 リンパ器官というだけあって、鼻や口から入ってきた細菌やウイルスをとらえる免疫の役割を持っています。

免疫機能が未熟な子どもは、扁桃の機能に頼る割合が高く、ウイルスや細菌に感染しやすいのですが、成長するにつれて扁桃以外の免疫機能が発達してくるために、感染しにくくなってきます。それにつれて、扁桃自体も徐々に縮小します。 ただし、幼少期に扁桃炎を頻発するなどで、大人になっても比較的大きなサイズを保つ人もいます。

ちなみに「扁桃」というのはアーモンドの種子の別称です。 アーモンドの種子に形がにているという理由でこの名前がつけられたらしいです。 かつては「扁桃腺」と呼ばれていましたが、「腺」というのは身体の中で分泌活動を行う器官の名称であり、以後の研究によって腺の役割ではない説が有力となったため、現在では「扁桃」と呼ばれています。

扁桃には「咽頭扁桃(アデノイド)」「耳管扁桃」「口蓋扁桃」「舌扁桃」の4つがありますが、一般に扁桃と呼ぶ場合、口蓋扁桃を指します。

口蓋扁桃摘出術とは

口蓋扁桃を切除する手術です。 以下を基準として、手術適応か否かを判断されます。

  • 習慣性扁桃炎
    • 年に3,4回以上の頻度で急性扁桃炎になるひと
  • 扁桃周囲膿瘍
    • 急性扁桃炎になりすぎて炎症が周囲に波及し、膿瘍になったひと
  • 扁桃肥大
    • 睡眠時無呼吸症候群で扁桃が大きい*2ひと
  • 病巣扁桃
    • 扁桃が他の疾患*3の原因になっているひと

自分の場合は習慣性扁桃炎と診断されました。 扁桃肥大ぎみでもあったのですが、頻繁に喉を起点とした風邪をひくためです。明確な検査があるわけではなく基本的に自己申告です。

経緯と注意点

かかりつけ医の診察から退院までの経緯をメモります。

診断〜入院

入院に至るまでに複数のステップがあります。

  • かかりつけ医で診断&紹介状取得
  • 大規模病院で診察&手術の予約
  • 術前検査(手術1週間前)
  • 検査結果説明&入院手続き

手術を受ける病院の選定

多くの場合、かかりつけ医からの紹介状をもって大規模病院で手術をうけることになります。 いきなり大規模病院*4に行くと、選定療養費として初診時5,000円がかかります。

どこの病院にするかはかかりつけ医と相談ですが、口蓋扁桃摘出術を都内で受けるなら、神尾記念病院JR東京総合病院あたりが有名なようです。ただ、口蓋扁桃摘出術は耳鼻咽喉科の手術としては基礎中の基礎らしく、どこで受けても特にリスク等違いはありません。手術前後で数回の通院を考慮して、自宅から近い病院にするのをオススメします。

自分の場合も自宅から近い東邦大学医療センター大橋病院にお世話になりました。2018年6月に新病棟が設立されたばかりで、とても綺麗でした。10日間ほど寝食を過ごすため、自分にとっては綺麗な病院というのはとても重要な要素でした。

入院までの心構え

重要なのは、入院直前に風邪を引かないこと、虫歯にならないことです。 ここまでの検査や10日間のお休み調整がすべて無に帰します。 虫歯がダメなのは、手術のときに口の中にいろいろと突っ込むので、悪い歯があるとさらに悪くなったり欠けたりするから、と説明されました。

費用と高額療養費制度の活用

口蓋扁桃摘出術はどの病院でも10日前後の入院を要するため、高額療養費制度が使えます。 これは、医療費が一定額*5を超えた場合に、その額を上限としてオーバー分を国が負担してくれる制度です。ここで注意されたいのは、以下の点です。

医療機関や薬局の窓口で支払う医療費が1か月(歴月:1日から末日まで)で上限額を超えた場合、その超えた額を支給する

例えば、10月25日に入院して11月4日に退院した場合、「10月25日〜31日」と「11月1日〜4日」はそれぞれ独立した医療費とみなされ、高額療養費制度の上限はそれぞれに適用されます。したがって費用を最小に抑えるためには、月をまたがない入院期間を設定する必要があります。自分の場合は1日オーバーして、トータルで10万円強の負担となりました。

また、入院は病室によってグレードがあり、例えば個室を希望すると高くなります。差額ベッド代は高額療養費に含まれません。病院都合で差額のあるベッドになる場合もあるので、初期段階で合意しておいたほうがいいです。高額療養費制度や差額ベッド代など、費用面について、病院側は信じられないくらい無頓着で、数万円は誤差くらいの勢いでこられます。一般庶民の我々が積極的に情報を収集し、交渉していく必要があります。

入院〜手術

お昼ごろに入院し、その日は手続きや検査などを済ませるのみでした。 手術はオンコールといって、呼ばれたら手術室に行くというスタイルのため、ソワソワしながら待っていました。結局14時ごろに呼ばれ、看護師さんに案内されて歩いて手術室に行きました。道中の風景はまさにドラマや映画でみるやつで、楽しかったです。

手術自体は全身麻酔なので、気づいたら寝ていて起きたらすべてが終わって自分のベッド、というかんじです。 点滴をつけたまま、喉の痛み止めを飲んで就寝です。

術後〜退院

手術翌朝、起きたらベッドとパジャマが血で染まっていました。ありがとうございました。

喉が痛すぎかつ発熱もあり、世界を恨みました。看護師さんはクールです。

入院後半は出血もなく食事も平常にとれる状態になったため、退院を希望したのですが、「術後一週間時点に出血リスクがある」とのことで、入院を継続しました。このあたりはおそらく以下の論文が論拠になっていると思われます。統計的に有意なのかという疑問はありますが、専門外すぎてわかりません。

ヒマはヒマなのですが、喉の痛みと微熱はあり、活字をよむのはなかなかしんどかったので、主にマンガを読んだりラジオを聴いて寝落ちしたりしていました。キングダムとバクマンを全巻読みました。最高におもしろかったです。ファルファル。

退院日は片づけと手続きを済ませ、午前中には自宅に帰っていました。翌日からは喉をいたわりつつも普通に過ごしました。

その後

退院から一週間後に経過観察のための受診をし、特に問題ないとのことで、運動やお酒も解禁されました。 退院直後は喉の違和感が残っていましたが、退院から10日が経ち、もうほぼ通常どおりに生活しています。

まとめ

体調が改善したかはまだわかりません。がんばっていきたいです。 なお、最近は扁桃切除せずに凝固術や抗生剤で治療するのが主流らしいです。 それらを試して改善が見られなければ、切除を検討されるといいかもしれません。

参考

*1:正確には「口蓋扁桃摘出術」

*2:マッケンジーの分類によりⅡ°以上

*3:掌蹠膿疱症・IgA腎症・胸肋鎖骨過形成症

*4:大学病院などの「特定機能病院」と、病床数500以上の「地域医療支援病院」

*5:収入に応じて決まる

技術書典5にサークル参加した記録

88年生まれ同い年の同僚たちと3人で、サークル名【88】として参加し、自分は『RDBMSのナカミ』という本を頒布しました。

BOOTHにも出品しています。

kabaome.booth.pm

pdfのみ122ページ、500円です。500円あれば100円均一の商品が5個買えると思いきや、消費税があるのでダメですね。話を戻します。ご興味ありましたらどうぞ。

参加の経緯

7月末頃、TwitterのTLで技術書典の存在を知りました。昼食を共にした同僚2名とその話になり、勢いで応募したら当選しました。技術書典どころかコミケなど同人イベントに一般参加すらしたことがなかったので、「"サークル"って何ぞ…?」くらいの状態でした。

各自思い思いのテーマで書き、当日までに3人とも仕上げて無事頒布することができました。よくがんばりました。我々。偉すぎ。

収支

目的は締切駆動学習ですが、せっかくの同人イベントなので、収支も簡単に紹介します。

当日76冊、その後BOOTHに出品し現時点で14冊をご購入いただいています。合計で90冊。よって売上は 500円×90=45,000円 です。一方、個人の費用は合計で約5,000円です。

  • 参加費(割り勘): 約2,300円
  • 見本誌製本、DLカード印刷: 約1,700円
  • ブース設営グッズ(割り勘): 約1,000円

以上より、40,000円程度の利益です。赤字にならなければ万々歳、くらいの気持ちだったので、まあ良かったです。

もちろん、これ以外に膨大な著者の時間と労力が投入されています。そういう意味では超絶赤字です。真紅です。利益を得る目的なら絶対にオススメしません。バイトとかしてください。

よかったこと

初のサークル初参加、諸々含めて、大成功だったと思っています。後世のためにやってよかったことを挙げてみます。

3人でやった

1人だったら確実にモチベーションが切れていました。RE:VIEWで書き、Bitbucketで管理して、Circle CIでEPUBをビルドして、Slackにポスト、としていたので、他者の進捗が通知されてきていいかんじに焦りを生成していました。

また、合同誌ではなくそれぞれ独立した本にしてよかったです。テーマが近しいならともかく、別々なら各自が責任をもつスタイルがよさそうです。お互いにレビューし合うこともでき、そのために前持った締切をお互いに設定しあえる環境もよかったです。最高の仲間。

割り切った

まず、応募直後の段階で紙をあきらめ電子版のみにしたのは完全に英断でした。締切が極度にタイトになり、生産コスト、在庫コストを抱えるのは、初参加かつお作法を知らない状態ではリスクが大きいです。当日の設営や撤収も瞬殺でした。「紙版があったら買ったのに」という損失は(0ではなかったですが)想定していたほどはなかったように感じます。

また、値段を500円均一にしたのもよかったです。オペレーションがシンプルでした。ちなみに他サークルをみるかぎり、500円はかなり安い部類です。原価がかからない電子版にしたからこその価格設定です。

毎日書いた

たとえ1行でも、帰宅が遅くとも、とにかくなにかを書いて進めました。自分は筆が遅い自覚があるし、未知のことを勉強しながら書いていたので、直前ブーストは確実に無理だとわかっていました。

また、これも自分の特性上、途中で途切れるとモチベーションもそれに応じて減衰するので、注意を維持する意味でもよかったです。

早めにした

紙を諦めたことで入稿の締切からは解放されましたが、諸々の納期を早めに設定し&共有したことで、初参加にもかかわらず直前や当日のバタバタはほぼなかったです。天才すぎますね。

他の2名はわかりませんが、自分個人としては、最終週は何もやることがない状態でした。印刷物など外部発注系は真の詰みがありますし、早めであればあるほど安いです。精神的にも安泰です。

余裕すぎて、技術書典直前の土日は温泉旅行に行っていました。そしたら台風が直撃して、飛行機が飛ばないかもってなりました。危うく技術書典に参加できないところでした。

f:id:kyabatalian:20181009220258j:plain
客室からみた風景(自慢)

ノリで応募した

結局のところこれです。やってみようかな〜と思ったらとりあえず応募してみて、当選したらやれる範囲でがんばる。それだけ。

所感

執筆、サークル参加の両面で、とてもよい体験ができました。執筆面では、知らないことや、わかっているつもりがまったくわかっていないことの連続で、もはや勉強にしかならなかったです。サークル参加の面では、自分が書いたものを目の前でいろいろな人が読む様を、眺めているだけで楽しかったです。

自分のテーマは超絶シブく、50代の太った男性が主なターゲット層だと思っていたのですが、港区という名の異世界から迷い込んだのかな?と思わせるようなメイクバッチリの淑女や髪型ガッチガチの紳士が、弊見本誌を鋭い目で眺めている姿をみて、嬉しくも不思議な気持ちになりました。

反省としては、もっと会話をすればよかったなと思いました。後半はたまにこちらから聞いてみたりしていて、そのやりとりに価値を感じました。言い出しっぺとしてはサークルメイツにいい体験をしてもらいたいという意図もあったので、最初からグイグイ聞けばよかったです。

全体的に、美少女ラブみたいな空気感だったり、サークル通行証の説明文がなにかアニメ?のパロディっぽい文章だったりするのが、ちょっと自分にはきつかったです。同人文化において自分がマイノリティという自覚はあるので、やめてほしいとかは別に思いません。

運営はじめサークル参加の皆さまに対しては本当に心苦しくも、買いたいと思う本がなく、戦利品は0冊です。自分は技術!最先端!同人文化!サイコー!みたいなテンションではないですし、技術的な情報は基本的に公式ドキュメントや一般の商業誌で十分です。これは、自分の本のテーマ自体にも現れていますが、自分は最先端のイケてる技術を追いかけるよりも、積み上げられてきた理論を知り理解することのほうが楽しいと感じるためかもしれません。

今後

次回の技術書典にもサークル参加するかもしれません。そんな元気はないかもしれません。そのときもたぶんテンションで決めるんだと思います。そういうものですよね、人生って。

一方で、自分のエンジニアとしてのアウトプットは、ソフトウェアやサービスでありたいという思いも湧いてきました。本を書いて終わるのではなく、そこで得られた知識をものづくりに活かしていきたいです。明日からまたお仕事がんばろう。優等生のコメントっぽい。

さいごに

現地でブースに立ち寄り、またBOOTHから買っていただいたみなさま、本当にありがとうございました!!!!!!感想、コメントなどなど、ツイートやブログなどでお知らせいただけると、吐くほど喜びます。

サークルメイツの本もBOOTHで買えます。こちらも合わせてどうぞ。

tkrtkhsh.booth.pm hokkai7go.booth.pm

PowerShellでマルチスレッド処理(仮)

夏が終わりそうで切ない。

切ないので、何の気の迷いか、PowerShellの激軽なメモを書いてみる。

やりたいこと

3秒待ってprocessの連番を出力するだけの処理。これを5回繰り返す際に、並列でやらせたい。

function Do-Process()
{
    param([int] $process_num)

    $tid = [threading.thread]::CurrentThread.ManagedThreadId
    Start-Sleep -Seconds 3
    Write-Host "theadId:$tid, process:$process_num"
}

まず、直列のほうを実行する。

function Test-Sync()
{
    foreach ($i in 1..5)
    {
        Do-Process ${i}
    }
}

$time = Measure-Command {
    Test-Sync
}

実行結果はいかのとおり。

PS C:\temp> .\parallel_test.ps1
theadId:14, process:1
theadId:14, process:2
theadId:14, process:3
theadId:14, process:4
theadId:14, process:5

1から5まで順番に、単一のスレッドで実行されている。これをマルチスレッド化したい。

Workflowを使ってやる

Workflowってなんぞ?って人は、Windows PowerShell ワークフローについてをどうぞ。

workflow Test-Parallel()
{
    foreach -parallel ($i in 1..5)
    {
        Do-Process ${i}
    }
}

$time = Measure-Command {
    Test-Parallel
}

実行結果は以下のとおり。

PS C:\temp> .\parallel_test.ps1
theadId:9, process:5
theadId:4, process:3
theadId:11, process:1
theadId:12, process:2
theadId:6, process:4

処理の順番はバラバラで、スレッドも異なっている。 マルチスレッドで処理されている。ようにみえる。

PowerShell 3.0の時点では、-parallel はシングルスレッドで処理されていたらしい。

blogahf.blogspot.com

MSのひとも以下のように言っている。

As an Activity author you can’t make any assumptions about what thread your activity will be invoked on or how that thread will relate to any other thread used by any other activity.
Windows Workflow Foundation (WF4) Activities and Threads – Ron Jacobs

謎。かしこい人、教えてください。

Background jobsでやる

PowerShell 2.0以降では、jobをbackgroundで起動できる。

docs.microsoft.com

Invoke-Asyncでやる

パフォーマンス的にはBackground jobsを使う方法に劣るが、スレッドの管理がラップされていてシンプルに書けるらしい。

gallery.technet.microsoft.com

補足

今回使ったWindows PowerShellは、長年Windowsにくっついていた歴史とぬくもりのあるPowerShellであり、.NET Frameworkでできている。WIndows PowerShellのWorkflowは.NET Framework 3.0で導入されたWindows Workflow Foundationという技術を基盤にしている。

一方で、.NET Coreを基盤としたPowerShell Coreもある。ただ、こちらにはWorkflowが移植されてない。

github.com

そのため、お手軽にはマルチスレッドの処理ができない。以下のようにがんばることになる。

winscript.jp

環境

Windows 10。

PS C:\temp> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.15063.1209
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.15063.1209
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

参考