-
※ その 2 のエントリからの続きです。(エントリが長すぎてポストできなかったため、分割しています。)
[③ Windows Azure ストレージサービス]
Windows Azure ストレージサービスとは、大規模・大容量の、高信頼性データストレージサービスです。内部的には、データを複数のサーバで分散・冗長化して保持するようになっているのですが、外から見た場合にはこれが巨大な一つのストレージシステムに見えるようになっている、というのが、この Windows Azure ストレージサービスです。SQL Azure データベースサービスと同様、実際のデータは少なくとも 3 つのノードに複製格納されているため、ひとつのサーバがクラッシュしても、データは生き残ることが可能になっています。
※ (注意) ”Windows Azure” という名を冠していますが、前述の Windows Azure コンピュートサービスとは全くの別物なので注意してください。
※ (参考) Windows Azure ストレージサービスでは、SQL Azure データベースサービスとは異なり、Geo-Replication (データセンタまたがりのデータ複製)が行われるようになっており、これによりディザスタ対策がなされるようになっています。(Geo-Replication は、各地域(北米、ヨーロッパ、アジア)内にある 2 つのデータセンタ間で行われ、地域をまたがる複製は行われません。)
通常、こうしたストレージシステムに対しては、ローカル HDD であれば普通の Windows エクスプローラで、リモート HDD であればファイル共有を通してアクセスすることになります。しかし、この Windows Azure ストレージサービスでは、HTTP REST プロトコルと呼ばれる、特殊な HTTP プロトコルでデータの読み書きを行います。
この HTTP REST プロトコルを直接ハンドリングするのは大変ですが、幸い、C# や VB などでアプリケーションを開発する場合には、Windows Azure ストレージサービスへアクセスするためのライブラリを使うことができます。このため、このライブラリを利用すれば、HTTP REST プロトコルを知らなくても、Windows Azure ストレージサービスへのデータの読み書きを実施することができます。例えば、コンソールアプリケーションから、Windows Azure ストレージサービスを読み書きするには、以下のようなコードを利用します。(※ このサンプルコードは特に理解しなくても大丈夫です。興味がある方は読んでください。本論からは逸れるため、今回はコードの詳細は説明しません。)
1: CloudStorageAccount storageAccount = new CloudStorageAccount(
2: new StorageCredentialsAccountAndKey("nakama000",
3: "sLyGGvOHJszS9wABrog4HhrxN+8ICH0A/diyMp.....JS/6Cm4S9c3TDH+CRRo8Tj5vIpgYfB7yArq1+xWjDSg=="),
4: new Uri("http://nakama000.blob.core.windows.net"),
5: new Uri("http://nakama000.queue.core.windows.net"),
6: new Uri("http://nakama000.table.core.windows.net"));
7:
8: CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
9: CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
10: CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient();
11:
12: // コンテナへの接続
13: CloudBlobContainer blobContainer = blobClient.GetContainerReference("pictures");
14: // コンテナの作成
15: bool created = blobContainer.CreateIfNotExist();
16: Console.WriteLine((created ? "コンテナを作成しました。" : "コンテナはすでに存在します。"));
17: // コンテナに対して、public アクセスを認めるように設定(http://.../Container/BlobName でアクセス可に)
18: var permissions = blobContainer.GetPermissions();
19: permissions.PublicAccess = BlobContainerPublicAccessType.Container;
20: blobContainer.SetPermissions(permissions);
21: Console.WriteLine("権限設定しました。");
22:
23: // ファイルのアップロード
24: string path = @"C:\Users\Public\Pictures\Sample Pictures";
25: foreach (string fullPathFilename in Directory.GetFiles(path, "*.jpg"))
26: {
27: string filename = fullPathFilename.Substring(fullPathFilename.LastIndexOf("\\") + 1);
28: byte[] data = File.ReadAllBytes(fullPathFilename);
29:
30: // まずファイルポインタを作成
31: CloudBlob blob = blobContainer.GetBlobReference(filename);
32: // すでに存在していたら削除
33: bool delete = blob.DeleteIfExists();
34: if (delete) Console.WriteLine("すでにデータがあったため、いったん削除しました。");
35: // そこに書き込みを行う ※ アップロード時にエラーがあってもちゃんと報告してくれないので注意
36: blob.UploadByteArray(data);
37: // クライアントに送り返すヘッダー情報を設定
38: blob.Properties.ContentType = "image/jpeg";
39: blob.SetProperties();
40: // サーバ側で保持するファイルのメタデータを記録
41: blob.Metadata["OriginalFilename"] = fullPathFilename;
42: blob.SetMetadata();
43: Console.WriteLine("ファイルを書きこみました。" + filename);
44: }
45:
46: // ファイルの一覧
47: var blobs = blobContainer.ListBlobs();
48: int i = 0;
49: foreach (var b in blobs)
50: {
51: // 各ファイルのメタデータを取得
52: CloudBlob cb = blobContainer.GetBlobReference(b.Uri.AbsoluteUri);
53:
54: // 属性データをサーバから取得
55: cb.FetchAttributes();
56: Console.WriteLine("{0} {1} {2}",
57: cb.Uri.AbsoluteUri,
58: cb.Properties.LastModifiedUtc,
59: cb.Attributes.Metadata["OriginalFilename"]);
60:
61: // データの読み取り(直接ファイルにダウンロードすることも可能)
62: cb.DownloadToFile(@"C:\temp\" + (i++).ToString() + ".jpg");
63: }
前述した Windows Azure コンピュートサービスを利用する場合、この Windows Azure ストレージサービスは、ログデータの転送先として非常に重要なものになります。これは以下のような理由によります。
Windows Azure コンピュートサービスを利用する場合、各仮想マシンは、インスタンス数の増減やサービスのフェイルオーバなどによって、簡単に起動したりシャットダウンさせたりすることができます(というより、それが Windows Azure コンピュートサービスの大きなウリです)。この結果、ローカルマシンに記録した内容(イベントログやパフォーマンスログ、ローカル HDD 上に記録したデータなど)は簡単に消え去ったり初期化されたりすることになります。このため、Windows Azure コンピュートサービスでは、基本的にはローカルマシンやローカルリソース(例えばローカル HDD など)にデータを記録・保存すべきではありません。……が、これではログファイルなどの記録や保存を行うことが全くできなくなってしまうため、困ってしまいます。
このため、各種のデータを残しておく目的で、以下の 2 つのストレージシステムを使います。
- SQL Azure データベースサービス : 主にトランザクショナルな業務データの保存に利用
- Windows Azure ストレージサービス : 主に各種のログファイルの保存に利用
このようにしていただければ、Windows Azure コンピュートサービスの仮想マシンインスタンスが増減したとしても、業務データやログデータが消失することはありません。
とはいえ、正直なところ、各種のログデータの出力先をすべて Windows Azure ストレージサービスにするようにアプリケーションを作ったり書き換えたりするのはなかなか大変です。この問題についても以下のような対処方法が用意されています。
応用的な内容になるため今回の一連のエントリでは解説しませんが、Windows Azure コンピュートサービス内の各仮想マシンには、Diagnostic Monitor ランタイムと呼ばれるサービスがインストールされています。このサービスは、イベントログやパフォーマンスログ、フラットファイルなどを定期的に監視・データ収集し、Windows Azure ストレージサービスへとデータを転送するようになっています。このサービスが存在するため、各アプリケーションから直接 Windows Azure ストレージサービスへデータを書き込む必要はなく、従来通り、イベントログやパフォーマンスログなどにデータを出力しているだけで済むようになっています。
ただし、既定ではこれらのサービスは動作していません。このため、構成設定や起動処理を行うことによって、このデータ転送機能を有効化する必要があります。Diagnostics Monitor が既定で転送できるデータの種類は以下の通りです。(転送処理を自力で作り込むこともできるようになっています)
※ (参考) Diagnostic Monitor ランタイムの使い方についてはここでは紹介しませんが、このサイトが非常に詳しいので、興味がある方は読んでみてください。(おそらく Diagnostic Monitor ランタイムを作っているチームの人だと思います)
※ (参考) なお、残念ながら現時点では Windows Azure ストレージサービスに転送されたログデータを簡単に見る方法(ビュアー)が提供されていないため、それらについては自作する必要があります。というか RTM したらログビュアーが提供される、という話だったような気がするんですがまだ提供されていないような……;
※ (注意) Diagnostic Monitor ランタイムによるログデータの転送機能は、遅延転送での動作になっています。このため、仮想マシンのクラッシュが発生した場合には、ログのとりこぼしが発生する危険性があります(通常の Web サーバでもマシンがクラッシュした際にはデータロストが発生するので当たり前のことですが;)。欠損してはならないデータの場合には、このログ転送の仕組みを使うのではなく、業務ログとして、SQL Azure データベースサービスなどに書き込むように設計してください。
さて、Windows Azure ストレージサービスでは、予め 4 種類のデータ構造が定義されており、それぞれの構造を持つデータを簡単に出し入れすることができるように設計されています。
- BLOB (巨大なバイナリデータ) : メディアファイルなどの格納に最適
- Table (ハッシュテーブル的なデータ) : キー付きのデータの保存に最適
- Queue (メッセージキュー) : Azure サーバ間の通信に利用
- Drive (NTFS ドライブ) : あたかも NTFS ドライブのように扱えるストレージ
これらについては比較的誤解があったり、わかりにくい資料も多いため、どんなものなのかを簡単に解説しておきたいと思います。
A. BLOB (巨大なバイナリデータ)
.wmv, .wav, .mp3, .jpg, .zip, .vhd などなど、主にマルチメディア系のファイルや巨大なデータファイルなどを格納するのに適したストレージ形式です。IIS ログファイルのようなただのテキストファイルに関してももちろん保存可能で、外から見た場合には 1 階層のファイルシステムであるかのように取り扱うことが可能です。
通常の Web サーバの場合には、FTP プロトコルでファイルをアップロードすることが多いと思いますが、この Windows Azure BLOB ストレージの場合には、HTTP REST プロトコルでファイルをアップロードすることになります。以下に概念図を示します。

なお、重要なポイントとして、各フォルダには public / private の設定を行うことができ、public 設定にした場合には、読み取りに関しては通常の HTTP-GET によるデータ取得ができるようになっています。(ファイル書き込みに関しては、HTTP-REST プロトコルでしか行うことができません。)
※ (参考) アクセス権限設定については、public / private の 2 択で、細かいアクセス可否設定はできません。残念;。
B. Table (ハッシュテーブル的なデータ)
Table ストレージは、プレインオブジェクト(POCO、プロパティ値のみを持つようなオブジェクト)を、お皿のようなものに入れて分散格納することができるストレージです。下図のようなイメージでとらえていただくとわかりやすいでしょう。
Windows Azure Table ストレージを利用するためには、各オブジェクトに対して、必ず以下の 2 つのキーを付与する必要があります。(正確にはこの 2 つに加えてデータ更新時の楽観同時実行制御用の Timestamp フィールドも必要になるのですが、まあとりあえずそれは置いておくとすると。)
- PartitionKey : データを複数のグループに分割するためのキー
- RowKey : 当該パーティションの中での一意識別キー
同一の PartitionKey を持つオブジェクトは必ず同一ノード(同一物理サーバ)で保持されるようになっていますが、異なる PartitionKey を持つオブジェクトは別ノードに保持されます。このため、データ検索処理を行うと、下図のように、複数のノードで分散検索処理が行われるようになります。
このことからもわかるように、Windows Azure Table ストレージを利用する場合には、PartitionKey の設計が極めて重要になります。うかつな PartitionKey を利用すると、検索速度などが極端に劣化することがあるため、注意してください。
なお、Windows Azure Table ストレージは、「テーブル」という名前がついているものの、いわゆる RDBMS のテーブルとは全くといっていいほど異なるものです。確かに、オブジェクトインスタンスを「行」、プロパティを「列」と捉えれば、確かにテーブル的な構造を持っているといえなくもありません。
しかし、以下のような点は全く異なります。
- テーブル間にリレーションシップを定義できない
- Join 処理も不可(単独テーブルへの出し入れのみ)
- トランザクション処理が基本的に不可(一連のデータの読み書きを 1 トランザクションに束ねることができない)
- 任意の列にインデックスを付与することができない
- 1 つのテーブルに、異なる構造のオブジェクトを格納できる
中でも最後の特徴は極めて重要です。Windows Azure Table ストレージでは、ストレージに対してスキーマの指定ができません。これは、Azure Table ストレージでは、(RowKey さえ異なれば)同一のテーブルに異なる構造を持ったオブジェクトを格納することができるためです。例えば、Author オブジェクトと Title オブジェクトという、異なる構造を持ったデータを同一テーブルに格納した場合を以下に示します。
この様子を無理矢理に表形式に書き直すと「穴空き表」になりますが、ストレージ内部で隙間だらけのデータとして格納しているわけではありません。実際には、左の図のように、「異なるオブジェクトがひとつのお皿の上に乗っているだけ」という状態になります。このような状態は、RDBMS のテーブルでは考え難いことですが、Windows Azure Table ストレージではごく当たり前のこととして扱われます。「テーブル」という名前にあまり惑わされないようにしていただければと思います。
C. Queue (メッセージキュー)
Windows Azure Queue ストレージは、その名の通り、メッセージキューシステムになります。以下のようなシンプルな機能を持ったメッセージキューシステムを提供します。
- やり取りできるデータ → string 型と byte[] 型のみ
- 送達保障・順序制御 → Exactly-Once のみ、In-Order なし
- リトライキュー・デッドキュー → なし
- メッセージ暗号化 → なし(キューに対する接続時の認証のみ)
機能は限定的ですが、その分、プログラミングは非常に単純です。
この Windows Azure Queue ストレージは、現在では利用シナリオが非常に限定的です。これは次のような理由によります。
- もともと Queue ストレージは、Windows Azure コンピュートサービスにおいて、Web Role サーバと Worker Role サーバを連携させるための通信経路として使うことを念頭において設計されていた。
- ところが現在では、Worker Role サーバが TCP/IP 通信を直接受け付けることができるようになっている。このため、Web Role サーバから Worker Role サーバへの単純な通信は、TCP/IP 直接通信で行えば済む。 (=一般的に非同期接続は設計・実装が大変になるので、わざわざ Queue ストレージを使って非同期接続する必要性はない)
すなわち、Windows Azure Queue ストレージを利用するのは、Web Role サーバと Worker Role サーバ間をキュー型接続(=非同期接続)しなければならない場合に限られる、ということになります(と書くとやや言いすぎですが、とりあえずはそう理解しておいても差し支えないでしょう)。実際にはそのようなケースはほとんどない(キュー型トランザクション処理が求められるケースは少ない)でしょうから、簡単なアプリケーションやシステムではほとんど使われることがない、と思っていただいてよいと思います。
D. Drive (NTFS ドライブ)
これは、中身としては Windows Azure BLOB ストレージなのですが、それをあたかも NTFS ドライブであるかのようにして使うことができる、という特殊な形式になります。アプリケーションから見た場合にはただの NTFS ボリュームであるかのように見えるため、通常のファイル I/O の API を使ってアクセスすることができる(ただし通常のローカル HDD に比べると若干アクセスが遅い)というものになります。現時点ではまだ未リリースのため詳細は不明ですが、今後、明らかになってくることでしょう。
というわけで、4 種類のWindows Azure ストレージサービスについて概要を解説してきましたが、重要なのは、どのようなケースでどのストレージサービスを使うのか、という点です。それぞれに適切な使い分けが必要になりますので、よく覚えておいていただければと思います。
[開発上の注意点]
さて、ここまで Windows Azure Platform の提供する主要サービスの概要を解説してきたわけですが、Windows Azure Platform の大きなメリットは、なんといっても
- オンプレミス型の Web アプリケーションの開発と、ほとんど同じ技術を使うことができる。
(Web アプリ → ASP.NET で開発、DB サーバ → SQL Server で開発)
という点でしょう。しかしこのことは、オンプレミス型の Web アプリケーションをそのまま Azure プラットフォーム上に移植できるということではありませんし、Azure プラットフォーム上での開発を、オンプレミス型の Web アプリケーションと完全に同じように捉えられる、ということでもありません。やはり、それ相応に相違点や注意点があります。
基本的に、Windows Azure Platform は PaaS プラットフォームです。このため、オンプレミス型の開発と異なり、ハード/ミドルなどのインフラに関わる設定や運用をほとんど考えなくて済むという利点があります。半面、Web サーバや DB サーバの設定やバージョンを自由に決められない・変更できないという注意点もあります。メリットとデメリットを比較する形で書くと、以下のようになります。
- 主なメリット
セキュリティパッチの適用、ハード障害時の対応、SQL Server や IIS の構成設定などを考えなくて済む。(すべてデータセンタ側にお任せすることができる)
- 主なデメリット(利用上の注意点)
タイムゾーン設定、言語設定、照合順序(並び順)、IIS や .NET の構成設定などが変更できないことが多い。ミドルウェアやパッケージ製品の追加インストールにも制限があり、配置するアプリケーションそのものにも制限事項がある。
主だった注意点を以下にまとめます。中でも、特に日本語まわりの注意点は、日本ローカルでアプリケーションを開発していたときとは大きく異なる部分になります。十分に注意して取り組むようにしてください。
このため、実際に Windows Azure Platform 上で動作するアプリケーションを開発する場合には、Azure のインフラ特性などをきちんと理解し、Windows Azure Platform の特性に併せた形でアプリケーション開発を行う必要があります。以上のような背景を考えると、Azure プラットフォームに適したアプリケーションと、適していないアプリケーションとがあると言えます。具体的には以下のようになります。
Windows Azure Platform に適したアプリケーション(クラウド化が適しているもの)
- 一般的なインターネットアプリケーション
- トラフィックが、時期、曜日、時間帯などにより大きく変動するアプリケーション
- ASP サービスとして展開しているアプリケーション
- 自社でインフラを持たないソフトウェア会社が開発するアプリケーション
Windows Azure Platform に不向きなアプリケーション(オンプレミス型が適しているもの)
- セキュリティポリシー上、社外に持ち出すことのできないデータを取り扱っているアプリケーション
- Windows Azure Platform の SLA (サービスレベル)では不十分なミッションクリティカルシステム
- SP や QFE などのソフトウェアバージョンを固定したいアプリケーション
Windows Azure Platform は、一般的な PaaS サービスに比べると敷居が低く、またハイブリッド型(オンプレミスとクラウドを併用する形態)などを取りやすいといったメリットがあります。しかし、あらゆるアプリケーションが Windows Azure Platform 上での動作に適しているというわけではありません。適切なシステム及び適切な部分に対して、上手にWindows Azure Platform を適用するようにしてください。
[本エントリのまとめ]
というわけで、本エントリをまとめる目的で、もう一度、Windows Azure Platform の主要構成要素を俯瞰図的にまとめておきたいと思います。
- ① Windows Azure コンピュートサービス
Web Role サーバ : IIS と .NET Framework がインストールされた Web サーバ
Worker Role サーバ : .NET Framework がインストールされた汎用サーバ
- ② SQL Azure データベースサービス
10GB の容量制限を持つデータベース、主に業務データを保存する
カスタムレプリケーションによる 3 多重化により 99.9% の高可用性を保障してくれる
- ③ Windows Azure ストレージサービス
主にログデータやバイナリデータを保存するためのストレージシステム
A. BLOB (巨大なバイナリデータ) : メディアファイルなどの格納に最適
B. Table (ハッシュテーブル的なデータ) : キー付きのデータの保存に最適
C. Queue (メッセージキュー) : Azure サーバ間の通信に利用
D. Drive (NTFS ドライブ) : あたかも NTFS ドライブのように扱えるストレージ
また、これらを利用した典型的な Web-DB アプリケーションの構成は、下図のようになります(コンピュートサービスは主に Web Role サーバを利用、ストレージサービスは主に BLOB, Table を利用)。
また、Windows Azure Platform は PaaS 型プラットフォームであるが故に、オンプレミス型と比べて、ランタイムやミドルウェアの設定を自由に変更できないという問題点があります。このため、ASP.NET Web アプリケーションであれば、どのようなアプリケーションでも Windows Azure Platform 上に移植できる、というわけでもありません。Part 3. のエントリでは実装の詳細に触れていきたいと思いますが、「なんでもかんでもクラウド化」といった考え方をしないように、十分注意してください。
-
※ その 1 のエントリからの続きです。(エントリが長すぎてポストできなかったため、分割しています。)
[① Windows Azure コンピュートサービス]
Windows Azure コンピュートサービスとは、カスタムアプリケーションのホスティングサービスです。OS とミドルウェアがプリインストールされたマシンが提供されますので、そこにカスタムアプリケーションを乗せて実行する、という形になります。利用可能なサーバタイプとして、以下の 2 種類が用意されています。
- Web Role サーバ : .NET Framework と IIS がインストールされた Web サーバタイプのサーバ
- Worker Role サーバ : .NET Framework のみがインストールされたバッチアプリケーション用のサーバ

実際のシステムでは、この 2 種類のサーバを組み合わせて、ひとつのシステムを構成します。例えば、Web アプリケーションと Web サービスとバッチアプリケーションから構成されるシステムは、上記の 2 タイプのサーバを組み合わせて、以下のように組み上げることができます。
※ (注意) 上記の図では各サーバがあたかも物理マシンであるかのように書かれていますが、実際にはデータセンタ内では Hyper-V を使った仮想マシン(VM, Virtual Machine)として配置されます。
※ (参考) Worker Role サーバは、もともとバッチアプリケーション動作用のサーバを想定して開発されたために上述のような書き方をしました。しかし現在では、ポートをあけることによって、インターネット上から直接 TCP/IP 通信を受け付けることができるようになっているため、このタイプのサーバは比較的汎用性の高いサーバとして使うことができます。例えば、バッチアプリケーションに WCF ランタイムをホストさせれば、このサーバはアプリケーションサーバ化して使える、ということになります。
さて、Windows Azure コンピュートサービスは、「OS やミドルがプリインストールされており、アプリをそこに転送してインターネット上で動作させられる」という点において、各種のコンシューマ向け Unix/Linux 系レンタルサーバ(共用ホスティングサービス)に似ています。しかし決定的に異なるのが、各ユーザのアプリケーションが仮想マシンレベルで分離されている、という点です。
実際に Windows Azure コンピュートサービスを利用する場合には、以下の手順で利用します。
- 起動する仮想マシンのベースとなる OS イメージを選択する。
(※ 現在は Web Role サーバと Worker Role サーバの 2 種類のみが利用可能。) - そこに、パッケージ化されたアプリケーションをアップロードして動作させる。
Part 1. のエントリの FAQ の項で解説したように、このアプリケーション展開は、ファブリックコントローラと呼ばれるシステムにより行われます。こちらにも図を再掲しますが、Windows Azure コンピュートサービスの実態は、要するに仮想マシン(VM)のイメージのコピーと、アプリケーションを自動展開するシステムです。展開された各仮想マシンは、CPU やメモリとバインドされて動作する(通常は 1 インスタンスあたり 1 CPU がバインドされる)ため、サービスレベルの保障がしやすい形で動作することになります。
※ (参考) この割り当ての仕組みをプロビジョニング(Provisioning)と呼ぶのですが、プロビジョニングを司っているシステムがなぜ「ファブリックコントローラ」と呼ばれるのかも、この仕組みを知っているとわかりやすいと思います。「ファブリック(Fabric)」とは「織物」という意味なのですが、Windows Azure コンピュートサービスでは、「空いている物理マシンを探してそこに VM イメージのコピーを行い、Web Role サーバや Worker Role サーバなどを起動し、ひとつのシステムを組み立て上げる」という作業を行います。これはまさに、織物を組み立て上げるような作業と言えるでしょう。
なお、Windows Azure コンピュートサービスに関して真っ先に知っておくべき点として、ユーザが各マシン(仮想マシン)に直接デスクトップログオンして操作することができない、ということがあります。一般的な Unix/Linux 系のコンシューマ向けホスティングサービス(レンタルサーバ)では、
- FTP を使って、1 つずつファイルをアップロードしたり、書き換えたりすることができる。
- リモートシェルを使って、ファイルを読み書きしたりすることができる。
といったことが可能ですが、これらは Windows Azure コンピュートサービスでは一切できません。理由は様々にあるのでしょうが、一番大きな理由は、VM インスタンス数を簡単に増減できるようにするため、でしょう。一般的な Unix/Linux 系のコンシューマ向けホスティングサービスでは、1 台の Web サーバを複数のユーザが共有する、という形を取ります。このため、複数の Web サーバに負荷分散させるといったことはまったくできません。しかし、Windows Azure コンピュートサービスでは、各マシンにログインしたり、各マシンを個別にいじったりすることができないかわりに、以下のようなことができます。
- システムの処理キャパシティを上げたくなったら、VM インスタンス数を増やす。
- システムの処理キャパシティを下げたくなったら、VM インスタンス数を減らす。
特に、大規模トランザクションを処理するようなオンラインシステムの場合には、ピーク時のトラフィックと平常時のトラフィックに大きな違いがあるようなシステムが少なくありません。このようなシステムでは、従来ではピーク時のトラフィックに併せてサーバ台数を決定する、という設計を行っていましたが、これではお金がかかりすぎます。このため、その時点時点に見合ったリソースを用意すること、すなわちトラフィック量に併せて処理キャパシティを動的に増減させる(=繁忙期や繁忙時間帯のみ、仮想マシンの数を増やして処理キャパシティを一時的に増やす)ことでコストを抑えることがとても重要になります。このようなユーティリティコンピューティングを簡単に実現できるように設計されているのが、Windows Azure コンピュートサービスです。
※ (参考) これは私個人の感想なのですが、最初にこの仮想マシン割り当ての仕組みを見たときには、「なんて乱暴なシステムなんだ;;」と思ったのが正直なところです。というのも、オンプレミス型の Windows Server (IIS)の場合には、1 つの OS 上に複数のアプリケーションを配置する際、様々な配置オプションを提供していました(Web アプリケーション化による分離、アプリケーションプール/ワーカプロセスによる分離、CPU とワーカプロセスのバインド、Hyper-V、などなど)。ところが、Windows Azure コンピュートサービスは、「1 個の Web アプリケーションには 1 個の OS、1 個のワーカプロセス、1 個の CPU を割り当てる」(※ 厳密には VM サイズが指定できるのですが)、という極めて割り切られた設計がなされています。この結果、例えばスケーラビリティを向上させようと思った場合、オンプレミス型では非常に多くのオプション(スケールアップ、スケールアウト、プロセス数増加、スレッド数増加、etc.)が存在するのですが、Windows Azure コンピュートサービスでは、基本的に仮想マシンのインスタンス数を増加させるというオプションしか存在しません。この割り切り設計は非常に乱暴ですが、半面、極めて単純なスケーラビリティ調整システムが実現できるというメリットがあり、なるほどそう考えると極めてよく出来たシステムだなぁと思いました。(スケーラビリティに限らず、いろんなところにこの割り切り設計の良さが表れていますので、考えてみるとよいと思います。)
[② SQL Azure データベースサービス]
さて、Web アプリケーションの多くは RDBMS を利用しますが、Windows Azure Platform データセンタの場合には、SQL Azure データベースサービスと呼ばれる RDBMS を利用することができます。
※ (注意) SQL Azure データベースサービスで利用されるマシン群は、Windows Azure コンピュートサービスで使われるマシン群とは別物です。イメージとしては、同一データセンタ内の別のマシングループを使っている、と考えるとよいでしょう。(実際のところはどうなっているのかは私も知りませんが....) Windows Azure コンピュートサービスのインフラ上に、SQL Azure データベースサービスが乗っているわけではないので、ご注意ください。
(細かい違いを言い出せばキリがないですが、ざっくり言えば)SQL Azure データベースサービスは、物理的には、オンプレミス版の SQL Server 2008 に対して、いくつかのカスタマイズと制限事項を加えることによって作られたものです。このため、以下のような特徴があります。
- 通常のオンプレミス型の ASP.NET Web アプリケーションと同様に、ADO.NET を使って SQL Azure データベースにアクセスすることができる。
- SQL Server Management Studio から、Azure データセンタ内にある SQL Azure データベースサービスにつないで、データベースを管理することができる。(※ 現時点ではツールに一部制限あり)
非常に面白いのは、データベースアプリケーションの開発スタイルが従来の方式とほとんど変わらないという点です。例えばオンプレミス型の SQL Server に接続する場合やファイルアタッチデータベースを使う場合は、
- Server=sqlsvr2008;Initial Catalog=pubs;User Id=sa;Password=xxxxxxxx;Trusted_Connection=false;
- Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\pubs.mdf;Integrated Security=True;User Instance=True
といった接続文字列を使います。これに対して、SQL Azure を利用する場合には、これを
- Server=tcp:mbkz90u87g.database.windows.net;Database=pubs;User ID=nakama@mbkz90u87g;Password=xxxxxxxx;Trusted_Connection=False
といった接続文字列に変更します。たったこれだけの作業で、SQL Azure データベースにアクセスすることができます。ですから、コンソールアプリケーションから SQL Azure データベースサービス上に作成した pubs データベースにアクセスするためには、以下のようなコードを使うだけで OK です。従来のコードとほとんど変わりがないことがわかるかと思います。
1: SqlConnection sqlcon = new SqlConnection(
2: "Server=tcp:mbkz90u87g.database.windows.net;Database=pubs;
3: User ID=nakama@mbkz90u87g;Password=xxxxxxxx;Trusted_Connection=False;");
4: SqlDataAdapter sqlda = new SqlDataAdapter("SELECT * FROM authors", sqlcon);
5: DataSet ds = new DataSet();
6: sqlda.Fill(ds, "authors");
7: Console.WriteLine(ds.GetXml());
通常の業務アプリケーション開発の場合には、まずファイルアタッチデータベースを使って開発を進めておき、運用環境に移行するタイミングになったら、接続文字列を上記のような SQL Azure データベースサービス用のものに変更する、という形にすればよいでしょう。
※ (注意) 実際に SQL Azure データベースサービスにアクセスするためには、SQL Azure に対して TCP/IP 接続ができなければなりません。このため、① SQL Azure データベースサービス側のファイアウォール設定を変更する、② (イントラネットからアクセスしたい場合には)社内のファイアウォールを超えられるように設定をしたりツールをインストールしたりする、という作業が必要になります。自宅などで検証作業を行う場合には②の点は問題にならないでしょうが、多くの企業では、社内から社外に対する直接の TCP/IP 通信を認めていません。このため、みなさんの会社の中(イントラネット)から SQL Azure データベースサービスを使いたい場合には、何らかの対処が必要になることが多いです。
さて、SQL Azure データベースサービスに関してまず知っておくべきポイントとしては、以下の 2 つがあります。
- 1 アカウント(1 インスタンス)あたりの容量上限が比較的厳しい。
- データが 3 台のマシンに複製されており、99.9% のサービス可用性が保障されている。
まず、1 つ目のポイントに関してですが、ユーザは 1 つのアカウント(SQL Server でいうところのインスタンスに対応)内に複数のデータベースを作成できますが、その最大容量は Business Edition でも 10GB となっています。このため、データ量が多いシステムの場合には、以下のような対処が必要になります。
- 直近取り扱うデータのみを SQL Azure 上に置き、履歴データなどはオンプレミスの SQL Server 上に移すようにする。
- 取り扱うデータを分割し(例:顧客 ID 1,000 人ごとにグループ化し)、別々の SQL Azure データベースで取り扱う。(=アプリケーションレベルからデータをパーティショニングする。)
また 2 つ目のポイントですが、SQL Azure データベースサービスでは、ひとつのデータベースのデータを、物理的には 3 台のマシンに複製した形で保持します。これにより、どこか 1 台のマシンが破損しても、他のサーバで処理を引き継ぐことが可能になっており、99.9% のサービス可用性(ひと月あたりのダウンタイムが約 5 分)が保障されるようになっています。内部的には、以下のように動作しています。
- 外部からの TCP/IP 接続(SQL Server のネイティブ通信プロトコロである TDS プロトコルによる接続)は、いったん SQL TDS と呼ばれるサーバが受け付ける。このサーバがロードバランサとなり、特定のマシン(当該データベースを持っているマシン)に接続をルーティングする。(※ フェイルオーバ時は、この SQL TDS による接続ルーティング先が変更される。)
- クライアントから行われる更新処理に関しては、更新内容が 3 台のマシンに自動的に複製される。
- データ複製には、Microsoft がカスタムに作り込んだ特殊なレプリケーションシステムが使われている。(=ログシップやデータベースミラーリング、あるいは従来のデータベースレプリケーションなどを使ってデータが複製されているわけではない)
※ (参考) なお上記のデータ複製は、同一データセンタ内で行われます。このため、ディザスタ時(災害時、例えばデータセンタが地震で潰れた等)にはデータがロストすることになります。ディザスタを回避するためのデータバックアップや、別拠点へのデータバックアップについては、現時点では自力で行う必要があります。(将来的には同一データセンタ内またはデータセンタまたがりでデータベースを簡単にコピーできるようなツールやコマンドが提供される予定です。)
※ (参考) ちなみに、従来からあるデータベースレプリケーションの場合には、データ更新が別マシンに遅延反映されますが、SQL Azure データベースサービスで利用されているデータ更新はリアルタイムに行われます(=アプリケーションに対してコミット応答を返したときには 3 台のマシンに反映が終わっている)。このため、マシンクラッシュ時であっても、コミットされたトランザクションがロストすることはありません。となると、データベースエンジンとしての性能が気になるところですが、実際には、① データは同一データセンタ内の別マシンへ複製されている(=極端に離れたところに複製しているわけではない)、② データベースの最大容量が 10GB に制限されている、などの理由により、それほど大きな性能劣化にはなりません。
※ (参考) これらのことから分かるように、そもそも SQL Azure データベースサービスのようなクラウド型のデータベースサービスというものは、極めて高い性能要件(特に応答速度要件)を満たすことが原理的に難しいものです。SQL Azure データベースサービスはそもそも SAN のような高価なディスクストレージを利用していないでしょうし(おそらくただの 2.5” HDD でしょう)、さらにデータベースサーバ自体、複数のユーザによって共用されています。こうしたことを考えると、応答速度の保障が求められるようなシステム(例えばオンライントレードシステムのようなもの)には、クラウド型データベースは原理的に不向きである、と言えるでしょう。(ちなみに、極端に負荷の高い処理をしているユーザを強制的に遮断・切断する仕組み(スロットリングシステム)は持っていますので、タチの悪い他のユーザによってデータベースが使えなくなる、といったことがないようにはなっています。)
これ以外にも、データベースに関して様々な制約事項(分散トランザクションが使えない、ヒープテーブルが使えない、etc.)がありますが、これらについては後ほど記述します。ただ、制約事項はそれなりにありますが、一般に、高可用性データベースの運用というのは非常に難しく、コストもかかるものです。SQL Azure データベースでは、99.9% のサービス可用性を持つデータベースを、月額 10,000 円程度+トラフィック課金で利用できるわけですが、このサービスを使えば、データベースの監視もフェイルオーバ対策もぜんぶ Microsoft にお任せ可能。これは TCO 的な観点からすると激安である、と考えられるのではないでしょうか? 制限事項はあれど、使える部分には積極的に使っていきたいサービスではないかと思います。
では最後に、Windows Azure ストレージサービスについて解説します。
※ エントリが長いので分割しました。その 3 に続きます。
-
さて、Part 1. のエントリでは、マイクロソフトのクラウドコンピューティング戦略である “S+S” の全体像、そしてその中での Windows Azure Platform の位置づけについて解説しました。要点をまとめると、以下のようになります。
- マイクロソフトのクラウドコンピューティング戦略は、オンプレミス型のソフトウェアと、クラウド型のサービスとを上手に組み合わせて、最適なシステムを構築する、というものである。
- Windows Azure Platform とは、マイクロソフトによる PaaS サービスである。
- ユーザは、Windows Azure Platform 上に、ユーザアプリケーションを載せて動作させることができる。
Part 2. となる本エントリでは、Windows Azure Platform の概要について解説します。おおまかなトピックは以下の通りです。
- Windows Azure Platform とは何か
- Windows Azure Platform の主要構成要素
① Windows Azure コンピュートサービス
② SQL Azure データベースサービス
③ Windows Azure ストレージサービス - 開発上の注意点
では、順番に見ていきましょう。
[Windows Azure Platform とは何か]
Windows Azure Platform とは、ユーザが開発したカスタムアプリケーションを動作させることができる、PaaS サービスです。Web, DB から構成される典型的な Web アプリケーションを乗せて使うことができる他、TCP/IP 通信を受け付けることが可能なアプリケーションサーバなどとしても利用できるように作られています。概念図を以下に示します。
Windows Azure Platform の主要な構成要素は以下の 3 つです。
- Windows Azure コンピュートサービス
- SQL Azure データベースサービス
- Windows Azure ストレージサービス
さらに、これら以外の周辺的なサービスとして、Windows Azure AppFabric があり、さらに今後、Windows Azure VM Role や System Center Azure などの提供が計画されています(詳細や時期については不明)。これらのサービスを総称して、Windows Azure プラットフォームと呼びます。
※ (注意) 単純に “Windows Azure” または “Azure” という用語を使った場合には、① Windows Azure コンピュートサービスのみを指す場合、② Windows Azure コンピュートサービスと Windows Azure ストレージサービスの組み合わせを指す場合、③ Windows Azure Platform 全体を指す場合、の 3 通りの意味があり、混乱を招きます。本エントリでは混乱や誤解を避けるため、”Windows Azure” または “Azure” といった簡略用語を避け、きちんとした用語で解説したいと思います。
さて、自社開発アプリケーションを乗せるプラットフォームとして Windows Azure Platform を見た場合には、以下のような特徴(メリット)があります。
- .NET Framework や Visual Studio を使った開発ができる。
もちろん、 Windows Azure 特有の開発ルールや作法を守る必要はありますが、開発言語やツールなどをゼロから覚え直す必要はありません。これは大きなメリットです。 - 煩わしいハードウェア/ミドルウェア運用から解放される。
PaaS 型サービスなので当たり前なのですが、実際のところ、SQL Server などのミドルウェアの高可用運用は非常に大変です。特に SQL Azure データベースサービスに関しては、容量制限などはあるものの、99.9% (月間ダウンタイムが約 5 分)のデータベースを安価に利用できるというメリットは大きいと言えるでしょう。 - 利用に応じた課金がなされる。
これも PaaS 型サービスなので当たり前ですが、初期コストゼロ、繁忙期や混雑時間帯のみリソース増強を行うことができる、というのが大きな特徴です。例えばピザ屋さんなどの Web サイトでは、土日、あるいは夕方などにトラフィックが集中しますが、ピークトラフィックに併せてインフラを設計・保有すると、TCO が肥大化しやすくなります。Windows Azure Platform を利用すると、動的にサーバ数の増減を行うことができ、さらに利用時間に応じた課金となるため、相対的に安価にシステムを構築・運用できます。
もちろん、Windows Azure Platform にも弱点や難点はありますし、現在はできないことも多数あります。これらについては、「開発上の注意点」の項で解説することにしたいと思います。
なお、CTP の頃はデータセンタがアメリカ 2 拠点にしかありませんでしたが、2010 年からはアジア 2 拠点、ヨーロッパ 2 拠点がオープンしました。これに加えて、CDN (Content Delivery Network)が利用可能です。
※ (参考) 大規模システムやミッションクリティカルシステムに Windows Azure Platform を利用したい場合、設計上、Windows Azure コンピュートサービスと SQL Azure データベースに関しては、Geo-Replication (拠点またがりの自動複製)が行われない、という点に注意しておく必要があります。つまり、データセンタのディザスタ対策が自動的に行われるわけではありませんので、必要であれば自力で対処する必要があります。一方、Windows Azure ストレージサービスに関しては、同一地域内の 2 つのデータセンタ間(例えば香港とシンガポール間)での Geo-Replication が行われるようになっています。同一地域内とはいえ、物理的な距離としてはかなり離れていますので、ディザスタ対応がなされているとみなせるでしょう。
[Windows Azure Platform の主要構成要素]
さて、Windows Azure Platform 上に自社アプリを開発して乗せる場合には、主に以下の 3 つのサービスを利用します。
- Windows Azure コンピュートサービス(Web ロール、Worker ロール)
- SQL Azure データベースサービス
- Windows Azure ストレージサービス
ものすごくおおざっぱに言うと、以下のように理解するとよいと思います。(と、こんな乱暴に書くとマイクロソフトの中の人たちに怒られそうですが…;)
- Windows Azure コンピュートサービス : 要するに「Web サーバ」
- SQL Azure データベースサービス : 要するに「DB サーバ」
- Windows Azure ストレージサービス : 要するに「ネットワークファイル共有」
一般に、Web アプリケーションを動作させる場合には、Web サーバと DB サーバが必要になりますが、それぞれに対応するものが Windows Azure コンピュートサービスと、SQL Azure データベースサービスになります。そして、それとは別に、ログファイルを記録したり、巨大なデータファイルなどを格納しておくディスク領域(ネットワークファイル共有)が必要になる場合がありますが、それを担うのが Windows Azure ストレージサービスである、と考えてください。
※ (参考) Windows Azure Platform には、これらのサービス以外にも、Windows Azure AppFabric と呼ばれるサービスがあります。実際のシステム構築では、AppFabric に含まれる .NET サービスバスと呼ばれる機能が非常に重要になるのが、今回のエントリでは解説しません。
※ (参考) Windows Azure コンピュートサービスには、大別すると、PaaS 型のサービス(Web ロールおよび Worker ロール)と、IaaS 型のサービス(VM ロール)の 2 種類があります。VM ロールに関しては、現時点ではリリースされておらず、また現時点では詳細な仕様も明らかにされていないこと、また本エントリでは PaaS 型サービスのみを取り扱いたいと思いますので、VM ロールについては除外して解説します。
以下に、それぞれの構成要素についての概要を解説していきます。
※ エントリが長いので分割しました。その 2 に続きます。
-
※ その2 からの続きです。(エントリが長すぎてポストできなかったので分割しました。)
[クラウドコンピューティングに関する FAQ]
ここまでで解説はひととおり終了なのですが、よくある FAQ として、以下の 4 つの点を簡単にまとめておきたいと思います。
- 従来技術とクラウド技術の違い
- プロビジョニング
- パブリッククラウドとプライベートクラウドの境界線
- マイクロソフトのクラウドコンピューティング戦略の 4 つの柱
① 従来技術とクラウド技術の違い
その昔、「フレームワーク」というキーワードが流行した際、実態としてはフレームワークとは呼べないような製品まで「フレームワーク」という名称を付与して販売されていた時期がありました。この手の Buzz Word (流行語)はどうしてもマーケティング目的で利用されてしまうことが多く、「クラウド」というキーワードもやはりそのような側面があります。実際、オンプレミスとクラウドの境界線は、厳密には定義できないこともあり、用語の使い方が曖昧になっています。例えば下図のようなケースにおいて、各種のデータセンタはいずれも IaaS の一種のようなものとみなせますが、どこからクラウドと呼ぶべきか? 何をクラウドと呼ぶべきか? に関しては、今のところ、厳格な定義がありません。

ただ、おおまかに言うと、従来型のものとクラウド型のものには、以下のような違いがあります。
- 仮想化 : 各種のコンピューティングリソースが抽象化されている
- 従量課金 : 使った分だけお金を支払う
- 動的なリソース割り当て : 必要に応じて簡単にリソースを増やせる
この中でも、特に「動的なリソース割り当て」については、クラウド技術を特徴づける極めて大きなポイントになっていますので、これについて解説します。
② プロビジョニング
おそらく、「動的なリソース割り当て」を理解するためには、Windows Azure Platform の機能(サービス)のひとつである、Windows Azure コンピュートサービスの概要を理解するのが手っ取り早いと思いますので、これについて解説します。
先に述べたように、Windows Azure コンピュートサービスとは、Microsoft が提供する PaaS サービスで、OS とミドルウェアの部分が提供されるサービスになっています。その中身を、技術的な観点からものすごく単純に書くと、「仮想マシンの自動展開機能と、パッケージングされたアプリケーションの自動配置機能である」と説明できます。下図を見てください。
上図は、Windows Azure コンピュートサービスの内部動作をおおざっぱに示したイラストです。Windows Azure コンピュートサービスは PaaS サービスですが、具体的には以下のように動作します。
- ユーザは、開発したアプリケーションをパッケージングし、ポータルサイトからアップロードします。
- ファブリックコントローラは、データセンタ内のコンピュータリソースの空き状況を見て、IIS や .NET Framework がインストールされた OS イメージを、物理ハードウェア上に展開します。
- さらにここにアップロードされたアプリケーションを展開し、仮想マシンを起動します。
上記の一連の作業はすべて自動で行われるため、非常に素早くサービスを利用し始めることができます。また、リソースが不足した場合には、仮想マシンを追加して負荷分散することでキャパシティを増やすことができ、逆にリソースが過剰になった場合には、仮想マシンをシャットダウンすればよい、という形になっています。
このような、動的なリソース割り当ての仕組みは、一般的にプロビジョニング(Provisioing)と呼ばれています。プロビジョニングとは、「事前にリソースを用意しておき、ユーザからの要求に応じて適切な割り当てを素早く行う」ことを指す用語で、クラウドをクラウドたらしめる重要な技術要素のひとつになっています。
Windows Azure ではこれに加え、ファブリックコントローラがマシンの稼働監視やフェイルオーバなどを司っており、事実上の無人運転が行われているわけですが、ここまでの自動化が行われているクラウドサービスは、実際問題としては一部の企業に限られるでしょう。となると、何をクラウドと呼ぶのかの定義は、現実的にははっきりしない、というのが実態だと思います。
(参考) よく、「”Windows Azure” とはクラウド版の Windows OS である」という説明がなされますが、Windows Azure コンピュートサービスにおいて、実際に使われている OS は、我々がよく使っている Windows Server 2008 です。にもかかわらず、Windows Azure がクラウド OS である、と言われるゆえんは、上図のファブリックコントローラのところに肝があります。このファブリックコントローラは、データセンタ内の各物理サーバのリソースの空き状況を監視して、動的に仮想マシンをアサイン、展開する仕組みを持っています。このような仕組みは、クラウドコンピューティング特有のものである、と言えるでしょう。(つまり、Windows Azure のクラウド OS としての本体は、ファブリックコントローラである、と説明してもよいでしょう。)
(参考) Windows Azure コンピュートサービスの利用に関して、初期費用が発生しない、また「1 インスタンス・1 時間あたりの課金」という計算方式になっているのは、このような動的リソース割り当ての仕組みを持っているからでもあります。
③ パブリッククラウドとプライベートクラウドの境界線
何をクラウドと呼ぶべきかの定義がはっきりしない、というのと同様な話のひとつに、パブリッククラウドとプライベートクラウドの境界線がはっきりしない、という話もあります。本エントリでは、誤解を避けるために、オンプレミス/ホスタークラウド/メガクラウドという用語を使いましたが、これらの用語は一般的ではありません。一般的には、パブリッククラウド/プライベートクラウド/インターナルクラウドといった用語の方が有名でしょうが、これらは境界線がはっきりせず、またマーケティング的な都合から、やや解釈を広めにとって使われていることが少なくありません。
上図に示した境界線はどれが正しいと一概に言えるものではないでしょう。はっきりしていることは共用性が高いものをパブリックと呼び、低いものをプライベートと呼んでいることだけで、境界線がどこと決定しているわけではありません。Microsoft, Google, Amazon などが提供する超大規模クラウドサービスのみをパブリッククラウドと書くケースも多いようですが、ホスタークラウドでも汎用型かつ大規模であれば十分にパブリックなクラウドである、と言えると思います。このため、これらの用語を使う場合には、誤解が生じないように、意味するものが上記のどれなのかをきちんと示す必要があります。
④ マイクロソフトのクラウドコンピューティング戦略の 4 つの柱
ここまでの解説からわかるように、Windows Azure Platform とは、カスタムアプリケーションを乗せるためのインフラ(PaaS サービス)であり、”Windows Azure Platform” と “S+S(Software plus Services)” とは全くの別物であることもわかるかと思います。再度、クラウドコンピューティング時代においてマイクロソフトが提供するソフトウェアとサービスを、以下に掲載します。
この図から分かるように、ざっくり言うと、クラウド型として提供されるオンラインサービスは、以下の 3 つのブランドに大別されます。
- BPOS : SharePoint Online, Exchange Online, OCS Online など(ビジネスユーザ向け)
- Windows Live : Live Mail, Live Map, Live Messenger など(コンシューマユーザ向け)
- Windows Azure Platform : Windows Azure ストレージサービス、コンピュートサービス、SQL Azure (アプリケーション開発プラットフォーム)
これらに加えて、既存データセンタ(オンプレミス型)に対してクラウド関連技術を適用しようとする、ダイナミックデータセンタの 4 つが、マイクロソフトのクラウドコンピューティング戦略において重要な柱である、と説明できるかと思います。(ちなみにこれ以外にも、Windows Mobile や Dynamics CRM Online などもあります。興味のある方は調べてみてください。)
[まとめ]
というわけでここまでいろいろと説明してきましたが、要点をまとめると以下の通りとなります。
- マイクロソフトは、クラウドコンピューティング世代に対して全方位的なソリューションを展開している。これを¨"S+S" 戦略と呼ぶ。すなわち、オンプレミスソフトウェアとクラウドサービスの両方を併用することにより、柔軟かつ高度なソリューションを提供できるようになっている。
- インフラレベルから見るとシステム形態は 3 つに大別できる。
① オンプレミス型
② ホスタークラウド型
③ メガクラウド型
オンプレミス/クラウドはトレードオフの関係を持つため、適切な選択を行うことが非常に重要になる。 - マイクロソフトの提供するサービスのうち、特に重要なのは以下の 4 つである。
1. ダイナミックデータセンタ(既存データセンタのクラウド化)
2. BPOS (マイクロソフトの企業向けオンラインサービス)
3. Windows Live (マイクロソフトのコンシューマ向けオンラインサービス)
4. Azure Platform(開発者向けの PaaS プラットフォーム) - 今後は、オンプレミス向け技術と、クラウド向け技術の両方について、その特性や技術的差異をよく理解し、これらを併用したソリューション検討が必要になる。
なかなか全体像が掴みにくい “S+S” 戦略ですが、全体の枠組みを正しく理解した上で、適切な場所にクラウド技術を適用するようにしてください。
-
※ その1 からの続きです。(エントリが長すぎてポストできなかったので分割しました。)
[インフラから見た場合のシステム形態の分類]
さて、前述の図は、マイクロソフトが提供する製品やサービスを示したもの(=すなわちマイクロソフトの立場で書いたもの)ですが、今度はこれを、ユーザや SIer などの観点から見た図、すなわち利用方法や利用形態の観点から整理した図に描き直してみたいと思います。
まず、前述の図だと、システム形態としては「オンプレミスか、Microsoft が提供するサービスを使うか」の二択のように見えます。しかし、実際には、データセンタ事業者や SIer が Microsoft からパッケージ製品を購入し、これをクラウド型のサービスに仕立てて、エンドユーザや他の SIer に販売する、というモデルも考えられます。このため、インフラ的な観点からすると、クラウドコンピューティング時代におけるシステムの形態は、大まかに以下の 3 つに大別できます。
- オンプレミス型(自社保有・自社運用型)
エンドユーザが自らソフトウェアを購入し、自社内にシステムを構築する方法。これはクラウドとは呼べないが、自社内データセンタであっても、部分的にクラウド関連技術(例えば自動プロビジョニング機能など)が適用されることにより、運用の効率化などが図られていく形になります。 - ホスタークラウド型(データセンタ事業者によるクラウド)
データセンタ事業者が、特定の企業向けに提供するホスティング環境。オンプレミス型とメガクラウド型の中間型になりますが、業務システム構築の観点からすると柔軟性が高く、使い勝手がよいのが特徴になります。 - メガクラウド型("超"巨大なデータセンタによるクラウド)
数万~数十万台以上のマシンを保有してクラウド型のオンラインサービスを提供しているような「超大規模クラウド」を購入する方法。具体的には、Microsoft, Google, Amazon, Salesforce.com, Yahoo!, eBay などによるサービスが該当します。相対的には安価ですが、個別要件や要望にはほとんど応えてくれない、という問題があります。
(注意) 上記の図では、敢えて「パブリッククラウド」「プライベートクラウド」という用語を使っていませんが、これは、パブリックとプライベートクラウドの境界線の定義が曖昧であるためです。また、どこからがクラウドでどこまではオンプレミスなのか? といったことも同様の議論で、これらは言葉の定義の問題でしかない、と考えます。これらの議論に深入りすることは技術的に見た場合には不毛だと思うので、この資料では敢えて別の用語として、「オンプレミス型」「ホスタークラウド型」「メガクラウド型」といった用語を使うことにしました。この点は、FAQ にて後述します。
さて、クラウドコンピューティングに関しては、「クラウドコンピューティングが発展すると、中小のデータセンタ事業者が最も痛手を受ける」と言われます。この発言の意図するところは、「中小のデータセンタではメガクラウドのようなスケールメリットを得ることができないため、相対的に高価になってしまい、ビジネスが縮小する」、というものだと思います。しかし、個人的にはこの考え方に懐疑的です。というのも、前述したように、メガクラウド型のサービスにはカスタマイズ性(柔軟性)に難があることが多く、コストが安くても小回りが効かないことが多いからです。特に、業務アプリケーションの世界では、既存のシステムとの連携性、3rd party 製ミドルウェアの導入、高い SLA 要件などなど、個別最適化が求められる領域が多々あり、お仕着せ型のサービスではうまくシステム構築できないケースが多いと思います。逆にいえば、こうした個別要件への柔軟かつ高度な対応性という部分がホスター型クラウドの大きな魅力であり、そうであるからこそ、今後もデータセンタ事業者に対するホスター型クラウドのニーズは多々あるのではないかと思います。ただし、データセンタ事業者にもより一層のコスト削減圧力が働くでしょうから、自動プロビジョニングをはじめとする各種のクラウド技術を適用して、より一層高度なサービスを提供していくことが求められるようになると思います。
[利用者から見た場合のシステム形態の分類]
では次に、クラウドコンピューティング時代が到来することによって、利用者側にはどんな選択肢が増えるのかを考えてみたいと思います。
例1. .NET カスタムアプリケーションを開発し、運用する場合
この場合、従来はオンプレミス型で開発するか、またはホスタークラウド型で開発するかのいずれかを使うケースが多かったと思います。しかし、ここに新たにメガクラウド型で開発する、すなわち Windows Azure Platform 上で開発する、という選択肢が加わることになります。
ただし、先に述べたように、この 3 つの方法は等価ではありません。例えば、図だけ見ると、ホスター型で作られた .NET アプリケーションはそのままメガクラウド型に移行できそうに見えます。しかし、Windows Azure Platform は、データベースや OS の設定を自由に変更できません。このため、必ずしも簡単に移行できるというわけではないはずです。
また、現実的なシステムでは、これらを併用するハイブリッド型システムになることも多々あると思います。例えば、SLA 要件の厳しいシステムでは、基本的にはオンプレミス型やホスター型の形で作成しつつも、災害対策用のバックアップシステムとして Windows Azure Platform を併用する、という形態が考えられます。必ずしもこれらの選択肢は背反的なものではない、と考えるのが適切でしょう。
例2. メールサーバ(Exchange)を使いたい場合
メールサーバを使う場合も、メガクラウド型の SaaS サービスを使う方法、すなわち BPOS (Exchange Online)を使う、という方法が追加される形になります。また、ホスター型クラウドを使う場合には、IaaS 型だけでなく、HMC (Microsoft Solution for Hosted Messaging and Collaboration)を用いて作られた SaaS サービスを使う方法が考えられます。
※ (参考) HMC とは、簡単に言えば、事業者が Exchange サーバを SaaS サービスとして再販するために提供している、マイクロソフトのソリューションのひとつです。
ホスタークラウド型の SaaS と、メガクラウド型の SaaS サービスは、図だけ見ると同様に見えます。このため、一見するとあたかもホスタークラウド型は、今後、メガクラウド型の BPOS に取って変わるように見えますが、この考え方は正しくありません。というのも前述したように、メガクラウド型のサービスの多くは事実上カスタマイズが不可能であり、これは BPOS (Exchange Online)にも当てはまります。このため、ホスター型クラウドは、BPOS では対応できないようなきめ細かなサービスにより差別化が行われる形になります。お客様への提案の際には、まず BPOS はカスタマイズできないという前提条件の元で、BPOS の提供サービスに合致するか否かをまず判断します。そして合致しない場合には、BPOS をカスタマイズしようと考えるのではなく(それは無理です;)、ホスター型クラウドやオンプレミス型のいずれが適切なのかを考えていくことになります。
というわけで、このようにメガクラウド型のサービスという選択肢が増えることにより、ユーザから見たシステム構築手法も自ずと選択肢が増えることになります。しかし、なんでもかんでもメガクラウド型サービスを使うようになる、と考えるのは誤りです。システム要件に応じて、適切な使い分けあるいは併用をしていく必要があります。
[クラウドコンピューティング時代のビジネスチャンス]
では最後に、ここまでの話をまとめる目的で、クラウドコンピューティング時代の到来により、様々な事業者にどのようなビジネスチャンスが生まれてくるのかを考えてみます。それを考えることにより、新たなビジネスチャンスも見えてくるはずです。ここではご参考までに、私が所属する、マイクロソフトコンサルティングサービス(MCS)が提供しているサービスとして、どのようなものがあるのかについても軽く触れてみたいと思います。おそらく MCS が提供しているコンサルティングサービスを見ると、ビジネス的な狙いどころも明らかになってくると思います。
① オンプレミス型が中心のエンドユーザ企業の場合
オンプレミス型で構築したシステムを運用することが中心になっているエンドユーザ企業の場合、クラウドコンピューティング時代の到来により、以下のような選択肢が新たに生じることになります。
- ホスタークラウドやメガクラウドへの移行
インフラの自社運用を避け、ホスタークラウドやメガクラウドへ移行するパターンです。災害対策用のバックアップとして、ホスタークラウドやメガクラウドを利用する、ハイブリッド型クラウドへ移行するニーズも数多く発生してくると思います。この領域に関しては、MCS は ITAP (IT アーキテクチャ&プラニング)と呼ばれるサービスを提供しており、クラウドサービスを適用することによってどのようなシステム最適化が図れるのかを検討することができます。 - 自社データセンタへのクラウド技術の適用(インターナルクラウドの構築)
大量のサーバを抱えることによるスケールメリット(コスト低減メリット)は少ないですが、クラウド技術を自社データセンタに導入することにより、インフラ運用の効率化、動的かつスムーズなプロビジョニングなどを行えるようになるメリットがあります。この領域に関しては、データセンタ運用を効率化するためのツールキットである DDC Toolkit (ダイナミックデータセンタツールキット)が CodePlex から提供されていますが、このツールを用いてデータセンタ運用の効率化を図るためには、当該データセンタ運用の現状を見据えた上で、あるべき姿を模索する必要があります。このため、MCS では運用効率化のコンサルティングサービスを通じて、DDC Toolkit の適用などをコンサルティングしています。
※ (参考) DDC Toolkit というものをご存じない方も多いと思いますが、これはデータセンタ仮想化のためのツールセット(いわゆるライブラリ)であり、それ単体で導入可能なパッケージ製品にはなっていません。このため、DDC Toolkit を利用する場合には、実際には UI などの部分についてかなりの作り込みが発生します。Windows Azure Platform のような環境をポンと自社内に構築するためのお手軽ツールキットではありませんので、ご注意ください。
② データセンタ事業者の場合
クラウドコンピューティング時代の到来により、最も既存ビジネスに影響を受けるのがデータセンタ事業者である、とよく言われます。確かにそれはその通りですが、それは必ずしもビジネスの縮小を意味しないと思います。クラウドコンピューティング時代の到来により、以下のようなビジネスメリットやビジネスチャンスが生まれてくる、と思います。
- 自社データセンタへのクラウド技術の適用
これは①で述べたものと同様の話です。スケールメリットは少ないですが、より素早いリソース割り当て、低い運用コスト(初期費用やランニングコスト)などが実現できます。この領域に関しては、MCS は先に述べた運用効率化のコンサルティングサービスを提供しています。 - IaaS から PaaS, SaaS ビジネスへの進出
ほとんどの場合、従来からあるデータセンタの大半は IaaS 型サービスが中心でしょう。このような場合、利用者側の自由度は高いものの、半面、特にミドルウェアの運用の部分について、利用者側に大きな負担がかかります。そこで、Microsoft などからアプリを購入し、PaaS, SaaS としてサービスを販売(再販)するというモデルが考えられます。イラストだけ見ると、メガクラウド型サービスとして提供されている BPOS や Windows Azure Platform などに似ていますが、先に述べたように、メガクラウド型サービスには、小回りが利きにくいという弱点があります。そこを突く形でサービスを提供することにより、より高い付加価値をつけたサービス提供が可能になります。この領域に関しては、MCS ではデータセンタに関する IT 基盤の強化サービス、また実際に乗せることになる各製品に関する製品コンサルティングサービスを提供しています。
実際、現場のエンジニア側から見ると、ミドルウェア部分の運用監視をデータセンタ事業者側に移管できる、というのは極めて大きなメリットであることが多いです。特に、Windows サーバ系のデータセンタでは、OS のベースイメージのみが提供されるという簡易なサービス提供にとどまっている場合が多く、IIS や SQL Server などのミドルウェアの運用や監視まで含めたサービスが提供されると、利用者側にとっては大きなメリットとなるはずです。(ちなみに、どのようなサービスを作ればよいのかに関しては、Windows Azure Platform が提供する各種のサービス(例えば SQL Azure データベースサービスなど)が参考になるはずです。)
③ ISV(独立系ソフトベンダー)の場合
パッケージ製品などを作成している ISV の場合、従来から ASP 化(アプリケーションサービスプロバイダ化、すなわちオンラインサービス化)によるサービス拡販などが行われてきたかと思います。このような場合には、以下のようなビジネスチャンスが考えられるかと思います。
- メガクラウド型 PaaS サービスの利用
自社で開発しているアプリケーションを Windows Azure Platform 上で動作できるように修正し、メガクラウドのメリットを享受する、というものです。特にインフラやミドルウェアの運用監視の部分に関して大きなメリットが得られる(例えば SQL Azure データベースサービスを使うと、非常に安価に 99.9% の高可用性データベースを使うことができる)こと、またスケーラビリティに関しても大きなメリットが得られる(必要な料金を支払えば、キャパシティをすぐに拡大できる)ことについては、ISV にとっては大きな朗報となるはずです。この領域に関しては、MCS では Visual Studio Workshop を通じて、Windows Azure Platform 上でのアプリケーション開発ノウハウのスキルトランスファーを実施したり、あるいはその開発そのものを直接支援したりします。 - 各種のマーケットプレイスの活用
パッケージ製品のオンライン化では、サービスの販路の確保が重要になります。この点に関して、マイクロソフトではマーケットプレイスの強化を図ろうとしています。具体的には、Microsoft Pinpoint と呼ばれるサイトがあり、ここで様々なオンラインサービスやアプリケーションの販売をすることが可能になっています。ちなみに日本語版の提供時期は今のところ未定ですが、こうしたマーケットプレイスは、どちらかというと日本市場における販路確保というよりも、アプリケーションを多言語対応させて海外展開するための販路としての側面の方が重要でしょう(← ビジネスアプリケーションやサービスは、iPhone などのようにエンドユーザが多いわけではないためです)。自社アプリを多言語対応させて Windows Azure Platform 上に乗せ、各種のマーケットプレイスを通じて販売することにより、ビジネスの拡大を見込むことができます。
なお、Windows Azure Platform 上で動作するアプリケーションの販売方法は 2 種類ある、ということを知っておくとよいと思います。ひとつは、自社で Azure のライセンスを購入し、アプリケーションを動作させ、これを SaaS として販売するモデル。もうひとつは、自社のアプリケーションを Windows Azure Platform 対応製品として、パッケージ製品として販売するモデルです。(後者の場合、お客様は自前で Windows Azure Platform のライセンスを購入し、そこにアプリケーションを自分で配置して使う形になります。) Windows Azure Platform では、どちらの販売方式も可能になるようにライセンス形態が設計されています。
④ SIer (システムインテグレータ)の場合
おそらく、クラウドコンピューティング時代の到来によって最も大変なのが、SIer になると思います。確かに、お客様から受注してシステムを構築する、というビジネスの在り方そのものに大きな変化はないでしょうが、システム構築の際に、クラウド型サービスを活用するという選択肢が加わることにより、設計のパターン数が非常に増えることになります。
- システム構築における、ホスター/メガクラウドの活用
従来ですと、システム構築時にデータセンタを利用する、という一択で済んでいたものが、ホスター/メガクラウドの IaaS/PaaS/SaaS サービスの出現により、選択の幅が一気に広がることになります。具体的には、エンドユーザ向けにシステム提案をする際に、適宜、クラウドを活用する構成を含めることで、メガクラウドの利用による動的スケール調整や、ハイブリッド型システムによる安価な災害対策などのメリットを得られるようにしていくことになります。これにより、価格競争力のあるシステム提案を実施していくことができるようになる、ということです。この領域に関しては、MCS は一般的なシステム構築支援サービスを通じて、その中でクラウド技術を活用したシステムアーキテクチャ・アプリケーションアーキテクチャ提案を行っていく形になります。
以上、様々な事業者ごとのビジネスチャンスや狙いどころについて解説してみましたが、このように、クラウドコンピューティングやその周辺技術をどのように自社に生かしていくのかは、事業者の立ち位置やサービス内容によって全くといっていいほど異なります。自社のコアコンピタンス(最も強いところ)を見定めたうえで、クラウド技術をどのように生かしていくのかという、視野を一段高くした状態での検討作業が必要になる、と言えるでしょう。
※ 次のエントリへ続きます。(長すぎてポストできなかったので分割しました。)
-
さて、今回のエントリは Windows Azure Platform について解説するわけですが、Windows Azure Platform は実際のところ、既存のシステムの在り方をすべてリプレースするような、万能なサービスというわけではありません。このため、Azure を利用するにあたっては、そもそも Azure というものが、マイクロソフトの製品やサービスの中でどのような位置付けにあるものなのかを正しく理解することが重要です。そして、マイクロソフトの製品やサービスの中での Windows Azure の位置づけを理解するためには、マイクロソフトのクラウドコンピューティング戦略である “S+S” (Software plus Services)の概要と、それがもたらすコンピューティングシステムの変化を理解しなければなません。
そこで Part 1. となる本エントリでは、まず、マイクロソフトのクラウドコンピューティング戦略 “S+S” がどのようなものなのかについて、技術者向けに解説してみたいと思います。ちょっと長めのエントリですが、お付き合いいただければ幸いです。
[Agenda]
- クラウドコンピューティングとは何か
- マイクロソフトの製品とサービスの分類
- インフラから見た場合のシステム形態の分類
- 利用者から見た場合のシステム形態の分類
- クラウドコンピューティング時代のビジネスチャンス
- クラウドコンピューティングに関する FAQ
では、順番に解説していきましょう。
[クラウドコンピューティングとは何か]
非常におおざっぱに言うと、「クラウドコンピューティング」とは、ネットワークを介して提供される「サービス」を利用するシステム形態のことを指す用語です。ネットワーク(主にインターネット)の向こう側にあるサーバ群やシステムがどのような構成になっているのかはよく分からないけれど、ユーザが、そうしたサーバの中身やシステムの構成などを意識することなくサービスを享受することができる、というのがクラウドコンピューティングの肝になります。
上記は、「クラウドコンピューティング」という用語に関する教科書的な説明ですが、もう少し技術的に説明すると、以下のようになります。
一般的に、コンピュータシステムは、以下の 3 つのレイヤの積み上げにより動作します。(※ アプリ/ミドル/インフラの境界線は曖昧ですので、あくまで概念的なおおざっぱな分類だと思ってください。)
- インフラ : ハードウェア(PC、ディスクストレージ)、収容施設など
- ミドルウェア : OS や RDBMS、アプリケーションランタイムなど
- アプリケーション : 業務アプリケーションやビジネスアプリケーション、ゲームアプリケーションなど
従来のコンピュータシステム(例えばみなさんが今使っているパソコン)は、これらのすべてを自前で所有します。例えば、ハードウェアを購入し、OS を買ってきて、アプリをインストールして動かす。このような形態を、オンプレミス型のシステムと呼びます(上図の一番左)。
しかし、ホビーユースならともかく、ビジネスユースを考えた場合、ハードウェアの調達からアプリケーションの運用まで、すべてを自前で見なければならない、というのはとてつもなく大変です。クラウドコンピューティングでは、これらのすべてまたは一部を、他社に任せてしまう、ということを行います。このようにすることで、その企業が本来注力したい部分のみにビジネスリソースを割くことができるようになる、というのがクラウドコンピューティングの考え方になります。
クラウドコンピューティングには、どこまでの部分を『お任せ』してしまうのか、ということに関して、大別して 3 つのチョイスがあります。
- IaaS 型(Infrastructure as a Service)
インフラ部分のみお任せしてしまう、という方式。この方式を取った場合には、自社ではアプリとミドルのみ用意すればよく、インフラ部分(ハードウェアの調達や監視、故障対応など)は業者にお任せしてしまう、ということになります。 - PaaS 型(Platform as a Service)
インフラとミドルウェアの部分をお任せしてしまう、という方式。この方式を取った場合には、自社ではアプリケーションのみを用意する形になります。 - SaaS 型(Software as a Service)
インフラ、ミドル、アプリのすべてを外部委託してしまう、という方式。この方式を取った場合には、自社ではコンピュータシステムの構築や運用などについては一切気にする必要はなく、提供されるアプリケーションをそのまま使うだけ、という形になります。
……と、この説明を読んでみて、「これって従来あったサービスと何が違うの?」と思われた方も多いと思います。はい、概していえばその通りです。実は IaaS, PaaS, SaaS に関しては、似たようなサービスが従来から存在しています。例えば以下のようなものを考えてみましょう。
- ASP (アプリケーションサービスプロバイダ)
業務用のアプリケーションをインターネットなどを介してお客様に提供したりレンタルしたりするのが ASP ですが、これは典型的な SaaS と言えます。グループウェアや財務会計ソフト、給与計算、販売管理や在庫管理など多数のアプリケーションが、利用料金さえ支払えばすぐにブラウザから使えるようになりますが、これらはまさに SaaS そのものと言えます。また、Hotmail(Windows Live メール) や GMail なども、典型的な SaaS と言えます。 - ホスティングサービス
OS とミドルウェアが用意されたサーバを、お金を払ってレンタルし、この上に掲示板アプリなどを乗せて使うサービスが、主に Linux 系のサーバでよく見られます(例えばさくらのレンタルサーバ)。これは PaaS のようなものです。つまり、OS やランタイム(Perl や MySql など)がプリインストールされたサーバ上に、自分で作成した(あるいは他社から購入した)アプリケーション(掲示板アプリケーションやアクセスカウンタアプリケーションなど)を乗せて使うことができるようになっているわけですが、これはまさに PaaS 型のサービスであると言えます。 - レンタルサーバ
前述のホスティングサービスの多くはコンシューマ用ですが、ビジネス用には、サーバそのものを貸し出して自由にいじれるようにしたサービスも存在します(例えばさくらの専用サーバ)。これは IaaS の一種と言えます。つまり、ハードウェアの調達や故障対応、ネットワーク回線の手配などについては、事業者側で対応してもらう形にし、自分たちはそこに OS やミドルウェアをインストールし、アプリケーションを動かす、という形になります。これはまさに「インフラをサービスとして必要な分だけ購入する」というスタイルになります。 - データセンタ
データセンタは、「特定の会社の中の各部署にインフラサービスを提供する IaaS」である、と言えます。
学術的な定義はさておき、これらはいずれもクラウドコンピューティングのはしりのようなものです。また共通的な特徴として、利用した分だけ課金が発生する、というものもあります(このような課金方式はユーティリティコンピューティングと呼ばれています)。つまり、自社のコアコンピタンスではない部分については、外部の業者に委託し、利用した分だけの対価を払ってサービスを受ける、という形にしよう、というわけです。
※ (注意) なお、この説明だけ読むと「それだったら従来型のサービスも全部クラウド型のサービスって呼んでいいんじゃないか?」と思われる方もいるかもしれません。実際、例えば従来のホスティングに対して、クラウドは共有型ホスティングと呼ばれることもあります。しかし、クラウド型と従来型を隔てる極めて重要な技術的ポイントとして、プロビジョニング(自動プロビジョング)と呼ばれる機能があります。この機能を持っていることにより、クラウド型サービスは従来型サービスに比べて、非常に安価かつスムーズにサービスが提供できるようになっています。ちなみにコスト削減に関してはこれ以外にも、仮想化をはじめとする資源の共有(マルチテナント)、運用自動化や規模の経済など、惜しみない努力を図っており、これを従来型とクラウド型の違いとみなす人もいます。プロビジョニングがどのようなものであるのかについては、最後の FAQ のところで解説します。
[マイクロソフトの製品とサービスの分類]
さて、以上がクラウドコンピューティングにおけるサービスの分類ですが、マイクロソフトのクラウドコンピューティングに対する考え方や戦略は、ある意味、「全方位的」です。というのも、他社の場合、「もともと自社サービスのために作ったものを転用してクラウド型のサービスにしました」といった派生的なサービスが割と多いのですが、マイクロソフトの場合には、もともとソフトウェアをすべて自社開発している上に、データセンタまで自社で構築し始めてしまった(!)ぐらいです。このため、シングルベンダーでありながら、極めて多彩なサービスが提供可能になっています。半面、そのサービスがあまりにも多岐に渡るために、分類や整理が非常に難しく、全体像を把握するのが難しい、というのも実際です。
厳密性を追求するとかえってわかりにくくなるので、ここでは非常に乱暴に、マイクロソフトの製品やサービスを分類してみましょう。まず、マイクロソフトが提供する主要なパッケージ製品と、オンライン型のサービスを分類すると、以下のようになります。(※ 細かいところの正確性についてはここでは議論しないことにしてください;。また、点線部で示したところのように、欠けていたり、今後リリース予定だったりするところもあります。)
このような形で書いてみるとわかりやすいのですが、マイクロソフトは、各々の製品を、以下の 2 通りの方法でリリースしようとしています。
- オンプレミス用の製品(パッケージ製品) (= Software) (パッケージ購入・自社保有型)
- クラウド型のサービス(オンラインサービス) (= Services) (従量課金・自社非保有型)
しかも、これらは IaaS, PaaS, SaaS すべての領域に広がっており、極めて全方位的なラインアップを用意している、というのがマイクロソフトのクラウドコンピューティング戦略の特徴です。これを、”S+S” 戦略(Software + Services 戦略)と呼びます。
このマイクロソフトの戦略や考え方に関しては、メディアによっては「マイクロソフトはパッケージソフトウェアの成功体験から離れられないからだ」といった論調で語られることもあります。しかし、私個人の現場エンジニア感覚からすると、将来的にすべてのソフトウェアがクラウド型サービス(オンライン型サービス)になるという考え方や論調の方が、よほど非現実的だと思います。というのも、クラウドコンピューティング(オンライン型サービス)には、以下のような課題や問題もあるからです。
- データの秘匿性や監査性
例えば、金融機関の預金データや顧客データなどを、社外のパブリックなデータセンタ内(例えば Microsoft や Google のデータセンタ内)に置いてよいのか? と言えば、ふつうは No でしょう。セキュリティ要件上、取り扱うデータを外部会社に委託できないようなケースは実際のビジネスではよくある話です。(※ なお、これは Microsoft や Google のデータセンタ運用がセキュリティ的にずさんである、という意味ではありません。これらのデータセンタは極めて高いセキュリティで運用されています。しかし、サービスを利用する側の組織的なポリシー上、そもそも社外ににデータを持ち出せないケースがよくある、ということです。) - 極めて高い SLA (非機能要件への対応やその保証)
詳細は後ほど解説しますが、クラウドコンピューティングは、スケーラビリティ(動的なキャパシティ調整)に関しては極めて大きなメリットがあります。しかし、「性能要件(応答時間)を保証したい」「99.999% の高可用性を保証したい」といった、極めて高い SLA 要件は保証しにくい、あるいはできない、というのが実態です。 - サーバやソフトウェアのカスタマイズ性
上記のことと絡みますが、特に Microsoft や Google などが提供する、極めて大きなクラウド型のサービス(メガクラウドとでも呼ぶべきサービス)のほとんどは、個別要件への対応のためのカスタマイズに関して、大きな制約があることが普通です。例えば、業務アプリケーションでは .NET ランタイムのバージョンを特定のバージョン番号に固定したい、といったニーズがよくありますが、Windows Azure では、問答無用でセキュリティパッチやサービスパックなどが適用されていきます。また、サポートが英語でしか受けられない、といったこともよくありますし、サーバに自由にソフトをインストールできないといった制限もよくありますし、もっと身近なところでは、OS のタイムゾーンをいじれない(=時刻がグリニッジ標準時から動かせない)、既定の言語が英語になっている、といったこともあります。簡単に言えば、クラウド型のサービスを購入する、というのは、カスタマイズできない(あるいはカスタマイズが著しく制限された)パッケージ製品を購入するようなもので、利用者側の個別要件には基本的に対応してくれない、と考えておく必要があります(※ 実際にどこまでカスタマイズ可能かはサービスによって異なりますが、基本的にはカスタマイズ自由度が低い、と考えておくべきです)。 - ネットワークの接続性やデータの連携性
クラウド型サービスを利用する際によく問題になる点のひとつに、社内システムとの接続問題があります。例えば、イントラネットの業務アプリケーションの一部を社外のクラウド上に置く場合、クラウド上に置いたアプリケーションは、社内に置かれた Active Directory と連携してユーザを認証しなければならない、ということがよくあります。あるいは、クラウド上のデータベースから、定期的に社内のデータベースにデータをバックアップしなければならない、といったこともよくあるでしょう。このような場合、クラウド上のシステムと、社内システムとの接続をどのように行うか(そもそも行えるのか?)が問題になります。(※ ちなみに Microsoft の場合には、この問題に対処するために、サービスバスと呼ばれるサービス(AppFabric)や、Windows Identity Foundation による ID 連携サービスなどを提供しています。興味がある方は調べてみてください。)
つまり現実的には、クラウド型のソリューションだけではお客様要件を満たせないケースが多い、ということです。このため、実際のシステムは、オンプレミス、小型クラウドサービス(従来型に近いデータセンタのようなもの)、超大型クラウドサービス(Microsoft や Google などのデータセンタを使うもの)などを併用したシステム、すなわちハイブリッド型のシステムになるはずです。こうしたハイブリッド型のシステムを実現していくためには、ソフトウェアとサービスを上手に併用する必要がある、というのが Microsoft の “S+S” 戦略の根底にある考え方です。
(注意) クラウドコンピューティングのような新しいキーワードが出現すると、あたかもすべての既存のビジネスが新しいキーワードで書き変わるようなイメージで語られることがあります。しかし、こうした論調には十分注意が必要だと思います。クラウドコンピューティングは、よく、「蛇口をひねったら必要な分だけ水道水が出てきて、必要な分だけ水道料金を払うのと似たようなものだ」と言われます。しかし、水道水があったからといって、カミオカンデの水に水道水が使われるわけではないですし、銘酒の仕込み水が水道水になるわけでもないはずです(← 微妙に例えがわかりにくくてすみません;)。クラウドコンピューティングというパラダイムシフトは多くのビジネスやコンピューティングシステムの在り方に影響を与えるものではありますが、既存のビジネスやコンピューティングシステムをリプレースするものではない、と考えるべきです。
(注意) 前述したマイクロソフトの製品やサービスの分類図に関してですが、この図はあくまで単純な製品マッピングを示したものにすぎない、という点にも注意してください。先の図だけ見ると、あたかも BPOS というオンラインサービスが、Windows Azure Platform 上に構築されているかのように見えますが、そういうわけではありません。BPOS は BPOS の独自のインフラやプラットフォーム上にサービスが構築されています。このため、IaaS, PaaS, SaaS の正しいシステム構成図は下図のようになります。
(参考) これは個人的見解ですが、マイクロソフトテクノロジを採用する大きなメリットは、単一テクノロジで広範なシステム形態を利用することができる、というポイントだと思います。これは “S+S” 戦略などでも非常に明確に見て取れるのですが、エンジニアからすると、新たなシステム形態や技術が出現したときに、テクノロジをゼロから覚え直したり、異なるテクノロジにつなぎ合わせたりすることには多大な労力が必要になります。マイクロソフトテクノロジの場合、例えば Web アプリケーション開発であれば、オンプレミスの IIS 上でも、メガクラウド型 PaaS サービスの Windows Azure Platform 上でも、ほとんど同様のテクノロジが利用できる(ASP.NET や SQL Server/Azure など)ため、新技術が出現した場合でもそのラーニングコストや移行コストを最小限に抑えることができます(ゼロにすることは原理的に不可能ですが)。「最適なものを組み合わせて最適なソリューションを提供できるマルチベンダ方式」というのは聞こえはよいですが、現場のエンジニア感覚からすると、マルチベンダ方式の実態は、「最適なものを組み合わせるために様々なテクノロジを覚えねばならず、さらに組み合わせるための接続検証が必要になり、それらの組み合わせの動作保証までしなければならない方式」です。ソリューションが複雑化すればするほど、また新技術が多数現れてくるほど、マルチベンダ方式はかえってデメリットの方が大きくなる危険性がある、ということは覚えておいてください。(と、元 SIer にいたコンサルタントがつぶやいてみる。)
※ 次のエントリへ続きます。(長すぎてポストできなかったので分割しました。)
-
というわけで、みなさま明けましておめでとうございます。更新が滞っているこの blog ですが、面白いネタがないとなかなかエントリを作成できないのも本音なところ。がしかし、今年最初のビックニュースはなんといっても 1 月から始まった Windows Azure の商用ラウンチ。興味がある方も非常に多いと思いますが、「そもそも Windows Azure ってなによ?」というのがなかなか分からない、という方も多いと思います。実際、いろいろなところで話を伺うと、以下のものが混同されているような場合が少なからずありました。
- クラウドコンピューティング
- S+S (Software plus Services)
- Windows Azure (コンピュートサービス/ストレージサービス)
- Windows Azure Platform
そこで今回は、技術者向けに Windows Azure Platform がどのようなものなのかについて解説したいと思います。
※ あくまで技術者向けの資料として書きますので、ビジネス寄りの話や、お茶を濁すような書き方は敢えて避けたいと思います。(その方が、現場のエンジニアにとってはわかりやすいと思いますので。)
Part 1. マイクロソフトのクラウドコンピューティング “S+S” 概要 (その1, その2, その3)
- クラウドコンピューティングとは何か
- マイクロソフトの製品とサービスの分類
- インフラから見た場合のシステム形態の分類
- 利用者から見た場合のシステム形態の分類
- クラウドコンピューティング時代のビジネスチャンス
- クラウドコンピューティングに関する FAQ
Part 2. Windows Azure Platform 概要 (その1, その2, その3)
- Windows Azure Platform とは何か
- Windows Azure Platform の主要構成要素
① Windows Azure コンピュートサービス
② Windows Azure ストレージサービス
③ SQL Azure データベースサービス - 開発上の注意点
Part 3. Hello World, Windows Azure アプリケーションの開発
- コンピュートサービスで利用可能な仮想マシン
- アプリケーション展開の仕組み
- コンピュートサービスと従来サービスとの違い
- Hello World, Windows Azure アプリケーションの開発
- 開発ファブリック上での動作確認
- Windows Azure Platform 上へのデプロイメントと動作確認
※ なお、本エントリは 2010/01 現在の情報を元に書かれています。今後、Windows Azure Platform やマイクロソフトのクラウドコンピューティング戦略の強化や変更に伴って修正される場所も出てくると思いますが、悪しからずご了承ください。
※ また、クラウドコンピューティングに対する分類や考え方についてはまだ流動的であるため、少なからず nakama 独自の考え方や見解が含まれています。必ずしもマイクロソフトの公式見解というわけではない、ということを含みおいてエントリをお読みいただければ幸いです。
では、さっそく始めましょう。
-
さて Part 1. のエントリでは、業務処理の終了パターンの分類と、各アプリケーションタイプにおける基本的な実装パターンを整理しました。要点をまとめると、以下のようになります。
- 業務処理の終了パターンは、以下のように分類される。
- 突き合わせエラーについては、バックエンドのモジュール(BC や DAC)との連携によるチェック作業が必要になる。UI 部単体でチェックが可能なのは、単体入力エラーに限られる。
.NET Framework では、UI 開発技術として、ASP.NET, Silverlight, WPF, Windows フォームなど、様々なテクノロジが提供されています。これらの技術には、いずれにも、UI 部において、単体入力エラーチェックを効率よく実装していくための機能が備わっています。(これらの機能は、いずれも単体入力チェックを効率よく実装するための機能であり、突き合わせエラーのチェックや、システムエラーに関する対処を実装するための機能ではありません。いや無理矢理使えば使えるかもしれませんが;、それはこれらの機能が用意された目的や意図とはズレた使い方だと考えるべきだと思います。)
- ① ASP.NET Web フォーム : 入力検証コントロール
- ② Silverlight 3, WPF 3 : 例外ベースの双方向データバインド
- ③ Windows フォーム 2.0, WPF 3.5 : IDataErrorInfo ベースの双方向データバインド
さてこれらの機能は、いずれも「単体入力チェックを行う」「フィールド単位のチェックとインスタンス単位のチェックを行う」という点においては違いがありません。しかし、その実装方法や、エラーチェックに対する考え方は、全くといっていいほど違います。この実装方法の特性の違いを理解しておかないと、単体入力エラーチェックをうまく実装できないばかりか、開発生産性をかえって大幅に損なう結果に繋がりかねません。特に、ASP.NET Web アプリケーション開発の入力検証コントロールの使い方に慣れた人が、Windows フォームや WPF などのテクノロジを遣うと、おそらく入力検証のやり方が全くといっていいほど違うため、相当に戸惑うことになるはずです。(というよりも私はむちゃくちゃ戸惑いましたよ....orz)
本エントリの目的は、これらの各テクノロジにおける、実装パターンの違い(実装方法やエラーチェックに対する考え方の違い)を明確化することです。
- ① ASP.NET Web フォーム : 入力検証コントロール
検証コントロールを使って、「正しい文字列」を作成する方式 - ② Silverlight 3, WPF 3 : 例外ベースの双方向データバインド
双方向データバインドを使うものの、反映に失敗するケースがある方式 - ③ Windows フォーム 2.0, WPF 3.5 : IDataErrorInfo ベースの双方向データバインド
双方向データバインドを使うが、反映に失敗するケースがない方式
なお、以下に順番に各テクノロジの実装方式を解説していきますが、基本的にはどのテクノロジであっても、UI 部でやるべきことは以下の 3 つです。
- UI 上のテキストボックスなどから値を入力してもらう
- 入力された値を、コードビハインドのデータ変数に取り出す
- 単体入力チェックが済んだ値を、BC/DAC に送出する
実装テクノロジによる差異は、下線部のやり方の部分に出てきます。この点を意識しながら、以降の解説を読んでください。
※ (参考)なお本エントリは、各テクノロジでの単体入力エラーチェックの実装方法について、ある程度知識がある、という前提で解説を進めます。もし、各テクノロジでの単体入力エラーチェックの実装方法をまったく知らないという場合には、以下の情報を併読されることをお勧めします。
※ (注意)また本エントリは、各データ検証方式の考え方の違いを明確化することを狙っていますので、解説をかなり単純化しています。例えば、Silverlight 3 には、①に近いデータ検証を可能とする ValidationRule や、属性ベースでデータ検証を行う DataAnnotation などの機能が備わっていますが、これらについては触れません。詳細にデータ検証をご存じの方は「え゛ー?」とツッコミ入れたいところがたくさんあると思いますが、そこはちょっとだけ目をつぶっていただけるとうれしいです^^。
では、以下に順番に解説していきます。
[① ASP.NET Web フォームの場合:入力検証コントロール]
ASP.NET Web フォームの場合、単体入力チェックは検証コントロールを使って実装します。
- 4 種類の標準のチェックロジックが用意されています。
(必須入力チェック、フォーマットチェック、比較チェック、範囲チェック) - 上記の 4 種類でカバーできないチェックは、CustomValidator を使って自力で実装します。
(インスタンス単位の単体入力チェックなどは、CustomValidator で実装します)
この場合の、UI 部のコードビハインドの制御コード(ボタン押下のイベントハンドラのコード)は以下のようになります。
このコードについて、改めてじっくり考えてみると、以下のような特徴があることがわかります。
- ASP.NET Web フォームの検証コントロールは、「テキストボックスに、適切な値を作る」ように動作します。
- 検証コントロールによるチェックを通過できていれば(IsValid = true なら)、データ変数への取り出しや型変換などで失敗したりすることは絶対にありません。つまり、コードビハインド内で値をテキストボックスから取り出す際には、すでに単体入力チェックが終わっている状態になっている、ということになります。
- ただし、UI からコードビハインド内へのデータ取り出し作業自体は、自力で記述する必要があります。
上記のような特性は、Silverlight や WPF、Windows フォームなどとは全く異なります。
まず、一般的に、Silverlight, WPF, Windows フォームといった、リッチクライアント系のアプリケーション開発技術では、通常、双方向データバインドと呼ばれるテクニックを用いて、データ検証とデータ取り出しを同時に行います。
Silverlight, WPF, Windows フォームそれぞれで、双方向データバインドの実装方法は少しずつ異なりますが、根本にある基本的な考え方は、「UI コントロールの表示と、データソースオブジェクト間の値を、双方向にリアルタイムに同期させる」というものです。このため、双方向データバインドを利用すると、UI コントロールからのデータ取り出し作業(例:string customerName = tbxCustomerName.Text; などといった取り出し作業や、decimal price = decimal.Parse(tbxPrice.Text); といったパース処理)が不要となり、バインドされているオブジェクトを、UI から入力されたデータであるとみなしてそのまま使うことができます。これが、双方向データバインドを用いたデータ入力制御の根底にある、基本的な考え方です。
しかし、双方向データバインドにおける入力データの検証方法(単体入力チェック方法)に関しては、いくつかの方法があります。.NET Framework 内で使われている双方向データバインド時のデータ検証方法は、大別すると以下の 2 つに分類されます。
- ② Silverlight 3, WPF 3 : 例外ベースの双方向データバインド
- ③ Windows フォーム 2.0, WPF 3.5 : IDataErrorInfo ベースの双方向データバインド
これらは、単体入力チェックロジックを持たせる場所と持たせる方法に違いがあり、また双方向データバインドの挙動についても多少の違いがあります。このため、以下に順番に解説していきます。
[② Silverlight 3, WPF 3 の場合:例外ベースの双方向データバインド]
まず、Silverlight 3, WPF 3 の場合について解説します。これらの場合には、以下のようにして単体入力チェックロジックを実装します。
- バインドするオブジェクト側に、フィールド単位のデータチェックロジックを持たせる。
具体的には、下図 A のように、バインドオブジェクトのプロパティ setter に対して、フィールド単位のチェックロジックを持たせる。もし、UI から不適切なデータが投入された(テキストボックスから不適切な値が入力された)場合には、例外(通常は ArgumentException 例外)を throw し、値を受け取らないようにする。 - 双方向データバインドの "ValidatesOnException" 機能を使う。
具体的には、下図 B のように、UI 部(XAML コード)にて、バインドするオブジェクトの各プロパティと、UI 項目との紐付けを行う。これにより、UI 部から入力された値が、バインドされたオブジェクトに自動反映されるようになる。ここで、ValidatesOnException 機能を有効化しておくと、バインドオブジェクトのプロパティへの反映時に失敗した場合(=例外が throw された場合)、これをエラーメッセージとして赤枠やツールチップにより表示してくれるようになる。
(※ エラーメッセージを赤枠やツールチップ表示するためには適切なスタイル定義が必要ですが、これについてはサンプルコードを参照してください。)
A. 例外ベース双方向データバインドで利用する、バインドオブジェクトの実装例
B. 例外ベース双方向データバインドでの、双方向データバインドの実装例(UI 部)
さて、一見するとわかりやすそうなこの実装方法ですが、実際には厄介な問題を抱えています。それが、UI 上に実際に表示されている値と、バインドされたオブジェクトが持っている値とのずれです。
例えば上記のアプリケーションに対して、下記のような操作を行った場合(オブジェクトへの反映に成功したり失敗したりするケースが混在する場合)を考えてみてください。
- 顧客 ID として “3214” を設定する。(→ 反映に成功する)
- 顧客 ID を “12345” に変更する。(→ 顧客 ID は 4 桁英数大文字のため、反映に失敗する)
- 顧客名として “Nobuyuki” を設定する。(→ 反映に成功する)
- 生年月日として “1973/06/07” を設定する。(→ 反映に成功する)
- 生年月日を “1973/55/41” に変更する。(→ 日付として正しくないため、反映に失敗する)
この場合、UI 上に表示されている値と、バインドされたオブジェクトの中に設定されている値とがずれています。このため、業務処理のために UI から入力された値を使おう、と思った場合には、まず、双方向データバインドにエラー(反映失敗)があるか否かを確認する必要があります。バインドされたオブジェクトの中に入っている値をいきなり使うと、実は UI から入力された過去の正しい値を使ってしまうことがある、ということになってしまいます。
また、次のような問題もあります。一般的なデータエントリシートの場合、最初に画面を表示した際には何も記入されていないのが普通でしょう。しかし、そのためには、バインドされたオブジェクト側が空の状態(例えば null や空文字が入っている)でなければなりません。がしかし、このようなオブジェクトは、そもそも値として、本来正しくない値を抱えている状態になっています。
また、インスタンス単位の単体入力チェックを行うロジックについては、バインドオブジェクトに持たせることができません(この例だと電話番号と電子メールアドレスの少なくとも片方が入力されている、というチェック)。なぜなら、電話番号と電子メールの入力項目は、UI からずれたタイミングでひとつずつバインドオブジェクトに反映されてくるため、バインドオブジェクト側のフィールドに持たせることが困難だからです。
こうした事情から、例外ベースの双方向データパインドでは、UI 部のボタン押下のイベントハンドラを、以下のように実装することになります。
- まず、バインドにエラーが発生していないか否かをチェックし、フィールド単位の単体入力エラーがあるか否かをチェックする。
- 次に、バインドされたオブジェクトを見て、インスタンス単位の単体入力エラーがあるか否かをチェックする。
- 最後に、バインドされたオブジェクトに含まれるデータを使って、業務処理を行う。
つまり、ここまでの解説をまとめると、例外ベースの双方向データバインドの動作イメージは以下の通りになります。
- バインドエラーがない場合に限り、UI からの入力がすべてバインドオブジェクトに反映されている、という動作になる。このためイベントハンドラ内では、まずバインドエラーのチェックが必要。
- 仮にバインドエラーがなかったとしても、インスタンス単位のチェックをイベントハンドラ内で行う必要がある。
例外ベースの双方向データバインドでは、バインドオブジェクト側に、例外を使った検証ロジックを持たせているのですが、これは、バインドオブジェクトが不正な状態になることがないようにする、という考え方に基づいています。この考え方は、それだけ見ると、一般的なオブジェクト指向設計の考え方からして特に間違ってはいません。ところが、双方向データパインドは、UI 表示とバインドオブジェクトの内容との二点間同期を保つ、という考え方に基づいているため、根本的なところで概念的な相反があります。このため、上記のような厄介な実装上の工夫を行わなければならなくなるのだろうと思います。
しかし次に解説する、IDataErrorInfo ベースの双方向データバインドでは、このような概念的な相反は発生しません。
[③ Windows フォーム 2.0, WPF 3.5 : IDataErrorInfo ベースの双方向データバインド]
引き続き、Windows フォーム 2.0 や WPF 3.5 で導入されている、IDataErrorInfo ベースの双方向データバインドについて解説します。
IDataErrorInfo ベースの双方向データバインドでは、バインドオブジェクト側に、IDataErrorInfo というインタフェースを持たせます。このインタフェースは、オブジェクトインスタンス内部にエラーが含まれていることを、文字列情報として返すためのもので、これを使うことにより、前述の問題をきれいに解決することができます。
IDataErrorInfo インタフェースを持つバインドオブジェクトの実装例は後述しますので、まず先に概念図を示しましょう。IDataErrorInfo ベースの双方データパインドでは、以下のようにしてデータバインドを行います。
- 入力値が正しかろうと間違っていようと、とにかくオブジェクトに反映してしまう。
- オブジェクトインスタンスが不正な状態にある場合には、これを IDataErrorInfo インタフェースから公開する。
- これにより、常に UI とオブジェクト内の値とが同期される。
前述したように、双方向データバインドは、UI とバインドオブジェクトのデータを常に同期させる技術でした。この際、データとして誤りのある内容が UI から入力された場合にオブジェクトに反映させるのかどうか、が問題になったわけですが、IDataErrorInfo ベースの双方向データバインドでは、入力内容を常にオブジェクトに反映させます。すると、バインドオブジェクトが「単体入力エラーを含んだデータを抱える」ことになります。この単体入力エラーに関する情報を IDataErrorInfo インタフェースから公開させ、これを UI コントロールに拾わせて、画面上に表示を行う、ということをするわけです。
IDataErrorInfo インタフェースを持つバインドオブジェクトの実装コード例を以下に示します。
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.ComponentModel;
5: using System.Text.RegularExpressions;
6:
7: namespace WindowsFormsApplication1
8: {
9: public class CustomerInput : IDataErrorInfo
10: {
11: private Dictionary<string, string> _errors = new Dictionary<string, string>();
12:
13: private string _id;
14: public string ID
15: {
16: get { return _id; }
17: set
18: {
19: _id = value;
20: if (value == null)
21: {
22: _errors["ID"] = "ID は必須入力項目です。";
23: }
24: else if (Regex.IsMatch(value, @"^[0-9A-Z]{4}$") == false)
25: {
26: _errors["ID"] = "ID は半角英数大文字 4 文字です。";
27: }
28: else
29: {
30: _errors.Remove("ID");
31: }
32: }
33: }
34:
35: private string _name;
36: public string Name
37: {
38: get { return _name; }
39: set
40: {
41: _name = value;
42: if (value == null || value == "")
43: {
44: _errors["Name"] = "名前は必須入力項目です。";
45: }
46: else if (Regex.IsMatch(value, @"^[\u0020-\u007e]{1,40}$") == false)
47: {
48: _errors["ID"] = "名前は半角英数文字 40 字以内で入力してください。";
49: }
50: else
51: {
52: _errors.Remove("Name");
53: }
54: }
55: }
56:
57: private string _email;
58: public string Email
59: {
60: get { return _email; }
61: set
62: {
63: _email = value;
64: if (value == null || Regex.IsMatch(value, @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"))
65: {
66: _errors.Remove("Email");
67: }
68: else
69: {
70: _errors["Email"] = "電子メールアドレスとして有効な値を入力してください。";
71: }
72: }
73: }
74:
75: private string _phone;
76: public string Phone
77: {
78: get { return _phone; }
79: set
80: {
81: _phone = value;
82: if (value == null || Regex.IsMatch(value, @"(0\d{1,4}-|\(0\d{1,4}\) ?)?\d{1,4}-\d{4}"))
83: {
84: _errors.Remove("Phone");
85: }
86: else
87: {
88: _errors["Phone"] = "電話番号は (03)1234-5678 のように入力してください。";
89: }
90: }
91: }
92:
93: public DateTime? Birthday { get; set; }
94:
95: // 全体整合チェック
96: public string Error
97: {
98: get
99: {
100: if (_email == null && _phone == null)
101: {
102: return "電子メールアドレスか電話番号かのいずれか一方は必須入力です。";
103: }
104: else
105: {
106: return null;
107: }
108: }
109: }
110:
111: public bool HasErrors
112: {
113: get { return (_errors.Count != 0 || Error != null); }
114: }
115:
116: public string this[string columnName]
117: {
118: get
119: {
120: return (_errors.ContainsKey(columnName) ? _errors[columnName] : null);
121: }
122: }
123: }
124: }
コード中の 95 行目~122 行目が、IDataErrorInfo インタフェースにかかわる部分ですが、コードのポイントをピックアップすると以下のようになります。
- バインドオブジェクトの各プロパティは、たとえ単体入力エラーがあるデータであったとしても、とりあえずデータを受け取ります。かわりに、内部にエラー情報(エラーメッセージ)を蓄積しておきます。
- IDataErrorInfo インタフェースには、Error プロパティ(オブジェクトインスタンス全体にかかわるインスタンス単位の単体入力エラー情報を返すためのもの)と、プロパティ名を使ったインデクサ(フィールド単位の単体入力エラー情報を返すためのもの)があります。これらを使って、単体入力エラー情報を UI 部に対して返します。

Windows フォーム 2.0 を使う場合には、UI 側に ErrorProvider コントロールを張り付けておきます。このようにしておくと、ErrorProvider コントロールがバインドされたオブジェクトの IDataErrorInfo インタフェースから自動的にエラー情報を取り出し、画面上にエラーメッセージを表示してくれるようになります。(※ 実装方法の詳細は、こちらのエントリを見てください。)
また、バインドされたオブジェクトにエラーがあるか否かは、バインドオブジェクトのみを見れば簡単に調べることができます。このため、UI 部のイベントハンドラ(Button_Click イベント)のコードは、以下のように非常に簡単になります。
このように、IDataErrorInfo インタフェースベースの双方向データバインドを使うと、綺麗な形での単体入力データチェックが実装できます。全体像を示すと以下の通りになります。
スマートクライアントにおける、双方向データバインドと IDataErrorInfo インタフェースを用いた単体入力チェックロジックの実装モデルには、以下のような特徴があります。
- 単体入力チェック処理を、バインドオブジェクトに固めることができる。
このため、モジュールの役割分担が明確になる上に、単体入力チェックロジック部分だけを重点的に単体機能テストすることもできます。
- コードビハインドの記述が簡単になる。
コードビハインドのイベントハンドラでは、バインドオブジェクトだけを操作すればよく、UI コントロールを触る必要がなくなります。このため、コードビハインドのコードの見通しも非常によくなります。
- 入力仕掛り状態の維持が簡単にできる。
バインドオブジェクトをそのままシリアル化して保存すれば、入力しかけのデータをそのまま保存しておくこともできます。
実装モデルが非常に綺麗になるので、ぜひ覚えておくとよいでしょう。
※ (注意) このモデルは Windows アプリケーションなどでは有効ですが、Web アプリケーションでは有効ではありません。なぜなら、Web アプリケーションでは、データが入力される場所(=ブラウザ上)と、データを取り出す場所(=サーバサイド)が分かれており、UI からリアルタイムでデータを取り出すことができないためです。
[3 つの単体入力チェック方式の比較]
さて、ここまでの解説を整理しつつ比較してみると、3 つの単体入力チェック方式には以下のような違いがあることがわかります。
ここで重要なのは、単体入力チェックモデルの優劣を議論することではありません。というのも、ぶっちゃけ、どのモデルを使ったところで単体入力チェックは実装できるわけで、好みの違いはあれど、どのモデルがより優れている、といった議論は宗教論争になりかねません;。そうではなくて、自分が業務アプリケーションを実装する際に、どのモデルを使って単体入力チェックを実装しようとしているのかを意識することが重要です。実際、.NET Framework の中に標準で含まれるデータ入力検証フレームワークを見ても 3 通りはあるわけで(実は私が気付いていないもっと別のモデルもあるかもしれません…とつぶやいておく;)、これらをごちゃまぜにしたような実装は避けなければなりません。
アプリケーションを実装する際は、一貫性が非常に重要です。どの方式を選ぶにせよ、ある特定のアプリケーションの中では「このパターンで実装する」といった具合に、モデルを定めて実装するようにしてください。
※ (参考) さらに追加のつぶやきですが、よくこうした単体データ入力検証フレームワークに関して、「○○のタイミングでエラーメッセージを表示できるようにできませんか?」「○○のような方式でエラーメッセージを表示できるようにできませんか?」といったことを聞かれます。こうしたカスタマイズは、できる場合とできない場合とがあります。というのも、もともとフレームワークというものは、「動作モデルに制約を加えるかわりに、開発生産性を大きく向上させよう」というコンセプトで作られているものであって、「どんなふうに動作させるものであっても開発生産性がよくなるもの(万能薬)」ではないからです。もし、.NET Framework などが標準で備える入力検証フレームワークの動作ではお客様要件を満たせない、ということであれば、独自に単体データ入力検証フレームワークを作成するか、または既存の単体データ入力検証フレームワークにカスタマイズを加えるしかありません。一般には、こうした問題が極力発生しないように、UI 設計段階(=業務設計段階)から、ある程度実装効率というものを意識して、フレームワークの想定している動作に併せた形での設計を行うようにします。
[まとめ]
というわけで、ここまで .NET Framework が備えている各種の単体データ入力検証フレームワークに関して、その実装モデルの違いを解説してきましたが、最も重要なポイントをまとめると、以下のようになります。
- 単体データ入力検証フレームワークを使う上では、そもそも業務エラーとシステムエラーの分類や、単体入力エラーの分類を正しく行うことが必要になる。
- .NET Framework が備えている各種の単体入力エラーチェック機能は、下図の枠線内の実装(開発効率)を高めるためのものである。
また、単体入力チェックに対するアプローチは、ランタイムによってかなり異なります。
- ① ASP.NET Web フォーム : 入力検証コントロール
検証コントロールを使って、「正しい文字列」を作成する方式
- ② Silverlight 3, WPF 3 : 例外ベースの双方向データバインド
双方向データバインドを使うものの、反映に失敗するケースがある方式
- ③ Windows フォーム 2.0, WPF 3.5 : IDataErrorInfo ベースの双方向データバインド
双方向データバインドを使うが、反映に失敗するケースがない方式
これらはそれぞれに特徴があるので、データ検証に対する考え方をよく理解した上で活用することが重要です。本エントリを参考にして、さらに優れた業務アプリケーション開発を目指していただければ幸いです。
-
まず最初のエントリでは、「エラーチェック」とひとくくりにされている「エラー」を、体系的に分類することを試みてみます。このエントリでは、Web / Windows、あるいは Java / .NET などといった技術論とは無関係な部分についての解説を進めていきたいと思います。
- エラーチェック(ユーザ入力検証)の意味
- 正常終了/業務エラー/システムエラーの分類
- 業務エラーの細分化
- アーキテクチャから見たエラーチェックの実装場所
※ なお、本エントリで解説されている分類方法や命名方法は、あくまで nakama 個人の考え方・整理方法です。もしかしたらもっとよい設計パターンなどがあるかもしれませんので、その辺についてはあしからずご了承ください;。
[エラーチェック(ユーザ入力検証)の意味]
まずは、そもそもどのようなケースでエラーチェックが必要になるのか、ユーザ入力検証にどのような目的があるのかを考えてみましょう。
一般的に、業務アプリケーションは、参照系アプリケーションと更新系アプリケーションに大別されます。
参照系と更新系では、求められるアプリケーションの機能が大きく異なりますが、更新系アプリケーションでは、適切な入力エラーチェックをどのように実装するのかが、極めて重要なポイントになります。そもそもなぜ更新系アプリケーションでは、適切な入力エラーチェックが重要になるのか? ユーザ入力検証(エラーチェック)には、大別して以下の 2 つの目的があります。
- アプリケーションの保護
ユーザから入力された値をそのまま利用すると、エラーやセキュリティ脆弱性の原因になってしまうため、適切にフォーマットチェックなどを行う必要があります。(SQL 挿入、Cross-Site Scripting など) - ユーザビリティの向上
エンドユーザに親切なエラーメッセージを表示するように作成すると、使いやすいアプリケーションを実現することができます。
しかし、このようなユーザ入力検証機能(エラーチェック機能)を場当たり的に実装すると、開発生産性を大きく損なうことになります。このため、通常のアプリケーション開発では、ランタイムが持っている「ユーザ入力検証のための機能」をうまく活用して実装していくことが重要になるわけです。
こうしたランタイムのユーザ入力検証機能を利用する上で欠かせないのが、以下の 3 つのポイントに関する知識です。
- アプリケーションの終了パターンの分類
正常終了/業務エラー/システムエラーを正しく分類できること。
業務エラーが、さらに単体入力エラーと突合せエラーに分類されることを知っていること。 - アーキテクチャ的な観点から見た、エラーチェックの実装方法
論理 3 階層型アプリケーションにおいて、どこで何をチェックすべきかを判断できること。 - ランタイムが持つバリデーション機能(エラーチェック機能)の狙い
ランタイムが持つバリデーション機能のコンセプトの違いを理解していること。
どの部分をカバーする目的で作られているのか、どこまでできるのか、その限界点を理解していること。
本エントリでは、まずこのうち最初の、「エラーパターンの分類」の部分について解説します。
[正常終了/業務エラー/システムエラーの分類]
業務アプリケーションの「入力エラー」を考える上で欠かせないのが、ひとくくりに「エラー」とされているものを体系的に分類することです。これを考えるには、業務処理が終了するパターンを、以下の 3 つに分類するのが最初のスタートポイントになります。(※ この終了パターン分類は、nakama による勝手な分類で、業界標準でもなんでもありません;。あしからずご了承ください、とつぶやいてみる^^)
- 正常終了 : 特に問題なく、期待通りに業務処理が終了できた
- 業務エラー : ユーザ入力値の問題で、処理が完遂できなかった
- システムエラー : システムトラブルで、処理が完遂できなかった
業務アプリケーションの振る舞いを、まずこの 3 通りに分類することは極めて重要なので、具体例を取り上げながら考えてみましょう。
例えば、下図のような、重複顧客 ID を認めていないような、新規顧客登録業務を考えてみます。この場合、ユーザが画面からデータを入力し、ボタンを押下した場合の終了パターンは
- 正常に顧客情報を登録できたパターン → 正常終了
- 指定された顧客 ID がすでに他で使われていたパターン → 業務エラー
- データベースサーバに接続できなかった → システムエラー
の 3 通りに分類することができます。
この終了パターンの分類は、以下のように考えるとわかりやすいでしょう。一般的に、業務画面でボタンを押下した場合、その終了パターン(終了に応じた画面の表示方法)は、下図のように 3 通りに分けられます。この分類に当てはめれば、簡単に終了パターンを分類できると思います。
- 正常終了して、当該画面内に黒文字でメッセージを表示する。→ 正常終了
- 何かしらの問題があり、当該画面内に赤文字でメッセージを表示する。→ 業務エラー
- 何かしらの問題があり、別画面の「ごめんなさい」メッセージ画面(誰かがお詫びしているような画面、イメージ的には下図みたいな感じ^^)を表示する。→ システムエラー
[業務エラーの細分化]
さて、上述した業務エラーは、実はさらに以下のように分類することができます。(なお、以下の業務エラーの分類も、nakama による勝手な分類です。名称も一般的なものではないのでご注意ください;。)
- 単体入力エラー
ユーザ入力値「のみ」で正誤判定ができるエラー。これはさらに、フィールド単位の入力エラー(個々のフィールドのデータだけで正誤判定できるエラー)と、インスタンス単位の入力エラー(複数のフィールドを同時に確認しないと正誤判定できないエラー)とに分類できる。 - 突き合わせエラー
ユーザ入力値のみでは正誤判定できず、データベース上のデータなど、他のデータとの「突き合わせ」を行わないと正誤判定ができないエラー。
前述の、正常終了/業務エラー/システムエラーとまとめて分類パターンを示すと、下図のようになります。
この分類の考え方や方法は、Web アプリケーション/Windows アプリケーションを問いません。Web アプリケーションと Windows アプリケーションでは、これらのエラーの表示方法(ユーザへの通知方法)や実装方法が異なりますが、分類方法そのものは、どちらであっても変わりがありません。
そのことを示すために、以下のような、新規顧客登録画面(単票形式のデータ入力フォーム)を取り上げて、パターン分類をしてみましょう。
上記のような画面の場合、Web アプリケーション、Windows アプリケーションを問わず、ボタン押下に伴って発生する事象は、以下のように分類することができます。
このように、正常終了/業務エラー/システムエラーの分類方法は、アーキテクチャタイプ(Web アプリ/スマクラアプリ/Windows アプリ)などによって変化するわけではない、ということを覚えておいてください。
[アーキテクチャから見たエラーチェックの実装場所]
さて、前述の分類のうち、システムエラーに相当するものは、.NET では例外(Java の場合には実行時例外)と集約例外ハンドラを利用して実装します。この実装方法については、本 blog にイヤというほどしつこくまとめたので、以下のエントリをご覧ください。
簡単にいえば、.NET の場合には、システムエラーに相当するケースについては集約例外ハンドラでまとめて処理するため、個々のアプリケーションモジュール(UI/BC/DAC 部)にシステムエラーを後処理するロジックを実装する必要は(基本的には)ありません。(※ 例外処理を行うのは、フローチャートの流れを調整したい場合に限ります。これについては、上記の「.NET の例外処理 Part.2」に詳細に解説していますので、こちらを参照してください。)
では、前述した業務エラーの処理はどこに実装するべきでしょうか? こうした業務エラー処理を実装する際に、必ず守るべき、以下の 2 つの基本的な実装セオリーがあります。
- 業務エラーのチェックは、可能な限り、ユーザに近い場所で行う
エンドユーザにとって、「UI が即時反応すること」はユーザビリティ上重要です。このため、UI 部でできるチェックは必ず UI 部で行い、即座にエラーを表示するようにします。 - 信頼境界の端点では、必ずデータの再チェックを行う
信頼境界(Trust Boundary)とは、ネットワーク的に不正な攻撃を受ける危険性のある境界のことを指します。例えば、下図のような Web アプリケーションの場合、BC や DAC はリモートからの通信を受け付けるわけではないので、これらのモジュールが直接、不正なユーザから攻撃を受ける危険性はほとんどないと言えます。一方、UI 部分は、不正なユーザから攻撃を受ける可能性が極めて高いと言えます。このため、リモートからのネットワークアクセスを受け付ける場所では、受け取ったデータを必ず再チェックする必要があります。
先に述べたように、業務エラーは以下の 2 つに大別できますが、このうち、突き合わせエラーについては、UI 部のみで正誤判定することはできません。このため、突き合わせエラーは、バックエンドの BC や DAC と連携してエラーチェックを行うことになります。
- 単体入力チェック → UI 部のみでチェックが可能
- 突き合わせ入力チェック → BC や DAC と連携したチェックが必要
このことを踏まえ、業務エラー実装の基本セオリーを、論理 3 階層型の Web アプリケーションと、スマートクライアントアプリケーションに適用すると、業務エラーのチェック場所は以下のようになります。
A. Web アプリケーションにおける業務エラーの実装場所
まず、Web アプリケーションの場合には、単体入力チェックや突き合わせチェックを以下のように実装することになります。
- 単体入力チェックについて
ブラウザ上で JavaScript による単体入力チェックを実装するとともに、サーバ側の UI 部にも、単体入力の再チェックロジックを実装します。すべてのブラウザが JavaScript をサポートしているわけではないため、ブラウザ内(JavaScript)で単体入力エラーが発見された場合だけでなく、UI 部(*.aspx など)で単体入力エラーが発見された場合も、エラーメッセージを表示するように設計・実装します。このため、単体入力チェックはブラウザ内(JavaScript)、サーバサイド(UI 部)の二か所に、重複する形で実装します。(※ なお、ASP.NET の場合、単体入力チェックは、UI 部(*.aspx 上)に検証コントロール(バリデータ)を使って実装します。検証コントロールはクライアントブラウザ上でのチェックのために JavaScript を自動出力するようになっています。このため、*.aspx 上に一度貼り付けただけで、ブラウザ上でのチェック(ユーザの手元での即時チェック)と、サーバ側での信頼境界端点の再チェックの両方をしてくれることになり、開発生産性が大きく向上することになります。) - 突合せ入力チェックについて
突合せ入力チェックに関しては、BC, DAC 部で、業務処理の一部として実装します。具体的には、BC から UI 部に対して、業務エラー情報として返し、UI 部ではエラーラベルに表示を行うことになります。
※ なお、ASP.NET 検証コントロールを利用した単体入力チェックの手法については、拙著「Visual Studio 2005 による Web アプリケーション構築技法」の第 5 章「入力検証コントロール」を、また突き合わせ入力チェックの実装方法については、本 blog の 「.NET の例外処理 Part.1」の中の、「2 種類の業務エラー」と「具体的なシグネチャ設計例」の項を参照してください。
前述の例の場合、画面設計と実装コードは、おおまかに以下のようになります。(フルソースコードは、サンプルコードを参照してください。ここでは要点だけ示します。)
- UI 部の設計
- UI 部 → BC 部呼び出しの部分の処理コード
B. スマートクライアントアプリケーションにおける業務エラーの実装場所
一方、スマートクライアントアプリケーションの場合には、単体入力チェックや突き合わせチェックを以下のように実装することになります。(※ この実装パターンは、Silverlight などの RIA タイプのアプリケーションでも同じになります。)
- 単体入力チェックについて
UI 部に、双方向データバインドを使って実装します。また、SI 部(サービスインタフェース、具体的には XML Web サービス)が信頼境界端点になるため、SI 部にも単体入力チェックを重複実装する必要があります。ただし、先の Web アプリケーションの場合と異なり、SI 部で単体入力エラーが発見された場合にはシステムエラー扱いとします。これは、クライアント側で正しい UI アプリケーション(Windows フォームや WPF アプリケーションなど)を使っている場合、SI 部に単体入力エラーを含むデータが送られてくるということはあり得ないからです。 - 突合せ入力チェックについて
BC, DAC 部で、業務処理の一部として実装します。そして、SI から UI 部に対して、業務エラー情報として返し、UI 部ではエラーラベルに表示を行います。
前述の例の場合、画面設計と実装コードは、おおまかに以下のようになります。(フルソースコードは、サンプルコードを参照してください。ここでは要点だけ示します。また UI 部の詳細な実装は、さらに 2 パターンに細分化できるのですが、これについては Part 2 のエントリで解説します。ここでは「なんとなく」理解していただければ十分です。)
- UI 部の設計
- UI 部 → SI 部呼び出しの部分の処理コード
上記の解説をまとめると、以下のようになります。
このように、業務エラーのチェックを実装する場所・方法に関しては、守るべき基本セオリーが存在します。実装方式が異なっても、これらの基本セオリーは変化しないものですので、しっかり覚えておくようにしてください。
[本エントリのまとめ]
では、ここまでの解説をいったんまとめておくことにします。
- アプリケーションの終了パターンは、正常終了/業務エラー/システムエラーの 3 パターンに分類される。
- システムエラーは、例外と集約例外ハンドラによって処理する。このため、通常のアプリケーションコードの中には、システムエラーに関する処理コードは出てこない。(=例外をむやみに try-catch したり throw したりすることはしない。)
- 業務エラーは、単体入力エラーと突き合わせエラーに細分化される。単体入力エラーは、さらに、フィールド単位の入力エラーと、インスタンス単位の入力エラーに細分化される。
- アプリケーションアーキテクチャ的な観点から見た場合、業務エラーチェック(単体入力チェックと突き合わせ入力チェック)を行う場所には、基本セオリーが存在する。
アプリケーションの終了パターン分類
Web アプリケーションにおける業務エラーチェックの実装場所
スマートクライアントにおける業務エラーチェックの実装場所
さて、ここまではアプリケーションの一般的な設計原則論のようなものを解説しましたが、これらを実装する際には、ランタイムが持つ入力検証機能を活用していくことが望ましいと言えます。引き続き Part 2. では、これらのデータ入力チェックを、各ランタイムでどのように実装していくのかを解説し、それを通して、各ランタイムが持つ入力検証機能を比較してみることにしましょう。
-
というわけで久しくエントリをアップしていなかったこの blog ですが、最近、複数方面からお叱りの言葉が……; 忙しかったこともあってエントリをサボっていたこともあったのですが、ちょうどいいネタがなかったのも実際のところ。がしかし、先日 2009/9/26(土) に行った、わんくま同盟さんでの勉強会のネタが blog 化するにはちょうどいいだろう、という感じなので、その資料を使いつつ、blog エントリを書いてみることにします。
今回の解説ネタは、更新系業務アプリケーションで求められることになる、エラーチェックの実装パターンを体系的に分類してみる、というものです。ASP.NET や Windows フォームなどには様々なデータ入力検証のフレームワークがあるのですが、名称こそ同じ「データ入力検証」となっていても、データ入力検証に対する考え方は、フレームワークごとにかなり異なっています。そこで、本エントリでは、特に .NET Framework 標準のデータ入力検証機能である、以下の 3 つを横並びにして解説してみて、各データ入力検証にどのような食い違いがあるのかを解説してみたいと思います。
- ASP.NET 検証コントロール
- Silverlight 3, WPF 3 の例外ベースの双方向データバインド
- Windows フォーム 2.0, WPF 3.5 の IDataErrorInfo ベースの双方向データバインド
なお、今回のサンプルコードは以下になります。エントリ中では、キーポイントとなるコード部分についてしか触れませんので、詳細なコードについてはこちらのサンプルコードをご覧ください。
[Agenda]
では、順番に解説していきましょう。
-
さて、Part 1~3 の解説で、Windows フォームにおけるマルチスレッドアプリケーションをスクラッチで開発する方法について述べてきました。結論としては、実は Windows フォームにおけるマルチスレッドアプリケーション開発は恐ろしく厄介で面倒である、ということになると思うのですが;、とはいえ
- 長時間を要する処理があるため、どうしてもマルチスレッドアプリにしなければならない。
ということも当然あると思います。幸い、.NET Framework 2.0/Visual Studio 2005 以降では、BackgroundWorker コンポーネントをはじめとして、マルチスレッドアプリを比較的簡単に書けるようにするための各種のコンポーネントやツールセットがいくつか追加されました。最後にこれらについて解説して、4 回にわたるエントリを締めくくっていきたいと思います。
今回のエントリで解説する内容は以下の 3 つです。
- XML Web サービス呼び出しの非同期処理化
- WCF サービス呼び出しの非同期処理化
- BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化
なお、本エントリでは基本的な XML Web サービスの作り方・使い方に関する解説は行いません。*.asmx による XML Web サービス開発をご存じない方は、一般的な書籍や Web の情報などを参照してみてください。また、今回のサンプルコードはこちらになります。
では、順番に解説していきましょう。
[XML Web サービス呼び出しの非同期処理化]
ASP.NET 2.0 XML Web サービス(*.asmx)に対する Web サービス参照(.NET Framework 2.0 ベースのプロキシクラス)には、非常に簡単に XML Web サービス呼び出しを非同期処理化できる機能が備わっています。ここではこの機能を使って、長時間を要する XML Web サービス呼び出しを行う Windows フォームアプリケーションを開発してみます。
まず、新規に Windows フォームアプリケーションを作成し、そこに Web サイトプロジェクトを追加します。
次に、*.asmx ファイルを使って XML Web サービスを作ります。長時間呼び出しをシミュレートしたいので、Thread.Sleep() 命令を使って 5,000msec だけ待機するように実装しておきます。
1: <%@ WebService Language="C#" Class="WebService" %>
2:
3: using System;
4: using System.Web;
5: using System.Web.Services;
6: using System.Web.Services.Protocols;
7:
8: [WebService(Namespace = "http://tempuri.org/")]
9: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
10: public class WebService : System.Web.Services.WebService {
11:
12: [WebMethod]
13: public string GetMessage(string name) {
14: System.Threading.Thread.Sleep(5000);
15: return "Hello World " + name;
16: }
17: }
18:
実装が終わったら、Windows フォームアプリケーション側で XML Web サービス参照を作成します。なお、Visual Studio 2008 を利用している場合、既定では .NET Framework 3.0 ベースの WCF サービスプロキシが作成されてしまいます。このため、「サービス参照の追加」→「詳細設定」→「Web 参照の追加」を選択しして、.NET Framework 2.0 ベースのプロキシクラスを作成する画面を表示し、ここでプロキシクラスを作成してください。
プロキシクラスを作成したら、ボタンやテキストボックスなどを貼り付けて画面を作成し、いったんコンパイルを行います。すると、ツールボックス上に、XML Web サービスプロキシのクラスが現れますので、これを当該画面上に貼り付けます。
そののち、以下の 2 つのイベントハンドラを記述します。
- button1_Click イベントハンドラ
ボタンが押されたときに発生するイベントハンドラ。画面に貼り付けた Web サービスプロキシ(webService1)を使って、XML Web サービスの非同期呼び出しを開始させる。
- webService1_GetMessageCompleted イベントハンドラ
非同期呼び出しが終了した場合に呼び出されるイベントハンドラ。ここに、XML Web サービス呼び出しが終わったときの処理(画面表示など)を書く。
※ 後者のイベントハンドラについては、プロパティ画面の上の方にあるイナズママークをクリックした上で、プロパティ画面内の「GetMessageCompleted」の項目をダブルクリックすると、作成することができます。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: // 二重押し防止のためのコード
4: button1.Enabled = false;
5: textBox1.Enabled = false;
6: // XML Web サービスの非同期呼び出し
7: webService1.GetMessageAsync(textBox1.Text);
8: }
9:
10: private void webService1_GetMessageCompleted(object sender, WindowsFormsApplication1.localhost.GetMessageCompletedEventArgs e)
11: {
12: // 終了結果取り出し(XML Web サービス呼び出し中に例外が発生した場合には、e.Resultプロパティにアクセスした際に例外が発生)
13: string result = e.Result;
14: // 結果表示
15: label1.Text = result;
16: button1.Enabled = true;
17: textBox1.Enabled = true;
18: }
さらに、Program.cs ファイルに集約例外ハンドラを記述します。
1: static class Program
2: {
3: [STAThread]
4: static void Main()
5: {
6: Application.EnableVisualStyles();
7: Application.SetCompatibleTextRenderingDefault(false);
8:
9: Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
10: Application.Run(new Form1());
11: }
12:
13: static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
14: {
15: MessageBox.Show("システムエラーが発生しました。アプリケーションを終了します。");
16: // 通常は例外ロギングも併せて実施
17: Application.Exit();
18: }
19: }
以上で作業は終了です。実行してみて、以下のような挙動をすることを確認してみてください。
- XML Web サービスの呼び出し中に、ウィンドウがフリーズしない(きちんとドラッグできる)。
- アプリケーション起動後に ASP.NET 開発サーバを終了させて、ボタンを押下すると、ちゃんと集約例外ハンドラがフックされる。

← XML Web サービスを呼び出せなかった場合
内部動作の概念図を下に示します。この処理のキーポイントは、UI スレッドへの戻りが自動的に行われる、という点です。webService1.GetMessageAsync() メソッドにより、Web サービス呼び出し自体は背後のスレッド(具体的にはプールスレッド)上で行われますが、
- 呼び出しが正常終了した場合に呼び出される webService1_GetMessageCompleted() イベントハンドラは、UI スレッド上で呼び出される。このため、このイベントハンドラ内では自由に UI コントロールを操作してよい。
- 呼び出しが正常終了せず例外が発生した場合でも、webService1_GetMessageCompleted() イベントハンドラが UI スレッド上で呼び出される。そして、e.Result プロパティにアクセスした瞬間に、発生した例外がリスローされる。
という挙動をします。
この挙動の中でも後者は非常に上手いところで、このような機能があるため、特に追加のコードを書かなくても、XML Web サービス呼び出し中に発生した例外を、Application.ThreadException 集約例外ハンドラで捕捉することができます。よって、上記のようなコードだけで、XML Web サービス呼び出しを非同期化することができる(背後のタスクスレッド上で動かすことができる)のです。
※ (注意) ただし、この実装方法では、XML Web サービス呼び出しをキャンセルすることはできません。一応 XML Web サービスプロキシには .CancelAsync() というメソッドがあるものの、これは「まだ未送信状態だったら呼び出しを取り消す」というものです。このため、実際にタスクスレッドで XML Web サービス呼び出しが行われてしまった後に .CancelAsync() したところで、行われてしまった呼び出しは取り消せません(=確実な呼び出し取り消しができるメソッドではありません)。もともとこの問題は、タスクスレッドを使っている以上は原理的に発生するものなので、設計時に注意しておくことが必要です。
※ (注意&参考) また、本題からは若干それますが、プロキシクラスを画面に貼り付けて利用する場合は、URL プロパティを構成設定ファイルから自動的に読み取らなくなってしまうため、下図のようにして明示的に紐付けを行ってください。(プロキシクラスのコード生成ツールとの兼ね合いで発生するトラブルのようです。明示的に紐付けすればきちんと読み取るようになります。)
[WCF サービス呼び出しの非同期処理化]
では今度は、同じことを .NET Framework 3.0 ベースの WCF プロキシクラスで行ってみましょう。話を簡単にするために、サーバ側は上記のサンプルと同じく、*.asmx を使うことにして、クライアント側に(サービス参照の追加機能を利用して) WCF のプロキシクラスを作成します。

作成したプロキシクラスは(先と異なり)フォーム上に貼り付けることはできません。しかし、以下のようなコードを書くことで、先ほどと同じようにコーディングすることができます。
このように、WCF プロキシクラスの場合には、画面上に貼り付けることはできないものの、きちんと UI スレッド上で呼び出し終了イベントハンドラを呼び出してもらうことができます。
※ (注意) .NET Framework 2.0 ベースの ASP.NET XML Web サービスプロキシの場合には、画面上に貼り付けなければなりません。コード上で Completed イベントハンドラの登録を行うと、UI スレッドへの戻りが発生しないため、注意してください。
さて、ここまで Web サービス呼び出しを非同期化する方法について解説してきましたが、最後に、より一般的なタスクを簡単に非同期化する方法について解説します。
[BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化]
ここまでの解説からわかるように、Windows フォームにおけるマルチスレッドアプリケーションの難しさは、UI スレッドとタスクスレッド間での連携によるところが大きいです。この連携処理を簡素化するために .NET Framework 2.0 で導入されたのが、ここで解説する BackgroundWorker コンポーネントです。この BackgroundWorker コンポーネントは、UI スレッドとタスクスレッド(プールスレッド)との間の協調連携動作を支援するコンポーネントとして機能します。概念図を下に示します。
この概念図だけだとわかりにくいと思いますので、実際に BackgroundWorker コンポーネントを使って、長時間処理を背後で行う以下のようなアプリケーションを作ってみることにしたいと思います。
具体的な実装手順は、以下の通りです。(何をやっているのかをわかりやすく示すため、Step by Step で実装していきます。)
① UI の作成
- まずは画面上に 2 つのボタン、ラベル、プログレスバーを置きます。
- それぞれのボタンに、btnStart, btnCancel と名前をつけ、キャンセルボタンの Enable プロパティを false にしておきます。
- 画面上に、BackgroundWorker コンポーネントを貼り付けます。
② 長時間処理の作成
- btnStart_Click() イベントハンドラを作り、ここに、BackgroundWorker コンポーネントに対して非同期処理を開始する指示を出すコードを記述します。
- 次に、backgroundWorker1_DoWork() イベントハンドラを作り、ここに実際の長時間処理を記述します。
- 最後に、backgroundWorker1_RunWorkerCompleted() イベントハンドラを作り、ここに終了後の処理を記述します。
実際の処理の流れを以下に示します。重要なのは、UI スレッド → プールスレッド → UI スレッドの流れが自動的に制御される、という点です。従来だと、自力で .BeginInvoke() などを記述しなければなりませんでしたが、そうした処理はすべて BackgroundWorker が肩代わりしてくれます。
③ 起動パラメータと処理結果の引き渡し
さて、上記のサンプルだと、タスクスレッドの起動パラメータの受け渡しや、タスクスレッドの処理結果の受け取りがありません。これらのコードを追加すると、以下のようになります。
(30msec × 321 回なので 10 秒ぐらいかかります)
④ 進捗状態表示機能の追加
では次に、進捗状態を UI 上に表示する機能を追加します。進捗状態は、プールスレッドから UI スレッドへの通知が必要ですが、これを行うために、以下の 2 つの作業を行います。
- backgroundWorker1 の WorkerReportsProgress プロパティを true に変更する。
- backgroundWorker1_DoWork() メソッドの中に、進捗報告のためのコードを追加する。(backgroundWorker1.ReportProgress() メソッド)
- backgroundWorker1_ProgressChanged() イベントハンドラを追加し、UI に表示する。
このようにすると、進捗状態が UI に表示されるようになります。
ここで注意していただきたいのは、プールスレッドで動作している backgroundWorker1_DoWork() メソッドから、UI 更新を行う backgroundWorker1_ProgressChanged() メソッドを直接呼び出しているわけではない、という点です。
- プールスレッドからは、backgroundWorker1 の .ReportProgress() メソッドを叩き、backgroundWorker1 にスレッド同期を依頼する。
- backgroundWorker1 は、UI スレッド上で backgroundWorker1_ProgressChanged イベントハンドラを呼び出すように、内部で .BeginInvoke() 命令を利用する。
ここでもう一度、最初に示した内部動作の模式図を示します。
最初からの流れをもう一度追いかけてみると、
- BackgroundWorker コンポーネントを用いたタスクスレッドの起動
① UI スレッドから BackgroundWorker コンポーネントの RunWorkerAsync() を叩く
② BackgroundWorker が自動的にプールスレッドに処理開始要求を投入する
③ その結果、DoWork イベントハンドラに登録されたメソッド(backgroundWorker1_DoWork() メソッド)が、プールスレッド上で起動する
- BackgroundWorker コンポーネントを用いた進捗状況の UI への通知
① プールスレッドから適当なタイミング(なるべく頻繁に)で BackgroundWorker コンポーネントの ReportProgress() メソッドを叩く
② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入
③ その結果、ProgressChanged イベントハンドラに登録されたメソッド(backgroundWorker1_ProgressChanged() メソッド)が、UI スレッド上で起動する
- BackgroundWorker コンポーネントを用いた終了通知
① プールスレッド上で、backgroundWorker1_DoWork() メソッドが終了する
② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入
③ その結果、RunWorkerCompleted イベントハンドラに登録されたメソッド(backgroundWorker1_RunWorkerCompleted() メソッド)が、UI スレッド上で起動する
となります。つまり、UI スレッドとプールスレッドの橋渡しを、BackgroundWorker コンポーネントが行ってくれている、ということになるわけです。
改めて、どの処理がどのスレッド上で動作するのかをまとめてみると、
- BackgroundWorker コンポーネント上の各メソッドをどのスレッド上で叩くか?
① BackgroundWorker.RunWorkerAsync() メソッドは、UI スレッド上から叩く。
② BackgroundWorker.ReportProgress() メソッドは、プールスレッドから叩く。
③ BackgroundWorker.CancelAsync() メソッド(後述)は、UI スレッド上から叩く。
- BackgroundWorker コンポーネントに登録したイベントハンドラはどのスレッド上で動くか?
① DoWork イベントに登録したハンドラは、プールスレッド上で動く。(=UI 操作不可)
② ReportProgress イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)
③ RunWorkerCompleted イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)
ということになります。
では最後に、キャンセル処理についても実装してみましょう。
⑤ キャンセル機能の追加
Part 3. で述べたように、UI スレッドからタスクスレッドを強制的に停止させることはできないため、キャンセル処理は「UI スレッドからフラグを立てる」「タスクスレッドからフラグをチェックして自主的に止まる」ことになります。具体的には、以下の実装を行います。
- backgroundWorker1 の WorkerSupportsCancellation プロパティを true に変更する。
- btnCancel_Click() メソッドに、backgroundWorker1.CancelAsync() メソッドを呼び出す処理を記述する。
- backgroundWorker1_DoWork イベントハンドラ内(タスクスレッドの長時間処理の中)に、キャンセルフラグを(なるべく頻繁に)チェックする処理を入れる。
追加されたコードは赤字部分です。ここまでの解説が理解できていれば、容易に理解できるのではないかと思います。
※ ちなみに実際に実行すると、キャンセルボタンを押した直後にプログレスバーが停止しませんが、これは Vista 以降でのコントロールのアニメーションの変更によるもの(アニメーションの遅延により発生する)です。XP などで実行すると、停止したタイミングでぴたっと止まります。
※ あと、書き忘れましたが、タスクスレッド上の例外処理についても書く必要がありません。タスクスレッド上で未処理例外が発生した場合には、RunWorkerCompleted イベントハンドラにて、e.Result で結果を取り出す際に例外がリスローされるため、特に例外処理のコードを追加しなくても、上のコードのままで集約例外ハンドラで例外を捕捉することができます。
このように、BackgroundWorker コンポーネントを利用すると、UI スレッド ⇔ タスクスレッドのスレッドスイッチに関連する処理を書く必要がなくなり、コードもかなりすっきりします。しかし、どの処理がどのスレッド上で動作しているのかを正確に理解しないと、非常に危険であるのも確かです。先に示した動作模式図を意識しながら、アプリケーションコードを記述するようにしてください。
[本エントリのまとめ]
では最後に、本エントリのまとめです。
- ASP.NET 2.0 XML Web サービスのプロキシクラスは、フォーム画面上に貼り付けて使うことにより、Web サービス呼び出し処理を非同期処理化できる。
- WCF サービスのプロキシクラスは、非同期処理メソッドを追加して使うことにより、呼び出し処理を非同期処理化できる。
- 一般的なタスクについては、BackgroundWorker コンポーネントを使うことで非同期処理化ができる。
というわけで、4 回に渡ってマルチスレッドアプリケーションの開発手法について解説してきましたが、総じて言えば、
マルチスレッドアプリケーションを書くのはかなり難しい;。
ということになります。正しい知識を持って記述しないと、とにかくトラブルを引き起こしがちな技術になりますので、記述するのであれば十分な知識を持った上で、正しく記述するように心掛けていただければと思います。
※ なお、今回は Windows フォームに限定して解説を進めてきましたが、WPF や Silverlight にも同様な UI スレッド制限があります。WPF などを利用する場合には、こちらの MSDN マガジンのエントリなどを参考にしながら開発を進めていただければ幸いです。
-
さて、前回の Part 2. のエントリでは、タスクスレッド(UI の背後で動作させる処理を動作させるスレッド、すなわちマニュアルスレッドやプールスレッドの総称)の様々な起動方法について解説しました。主な方法として、以下の 4 つの方法がありました。
- マニュアルスレッドの新規作成
- プールスレッドの利用
- 非同期デリゲートの利用
- タイマの利用
さて、いずれの方法を利用する場合であっても守る必要のあるルールとして、UI スレッド以外から UI コントロールの読み書きをしてはならない、というものがありました。
ここまで実際にプログラミングをしてみた方は感じられていると思うのですが、実はこの制限はかなり厄介です。例えば、以下のような処理を簡単に書くことはできません。
- 背後で行っているタスク処理の進捗状況や完了結果を、UI 上に表示する。
(これはすでに解説済み。.BeginInvoke() 処理が必要。) - 背後で行っているタスク処理から、UI 上のコントロールのプロパティを読み出す。
(タスクスレッドから、textBox1.Text プロパティを読み出すことは NG だがどうすればよい?) - 背後で行っているタスク処理を、UI 上のキャンセルボタン押下により中断させる。
(どうやって UI のイベントハンドラから背後のスレッドに通知を行えばよいのか?) - 背後で行っているタスクスレッド上で発生した例外の情報を、UI 上に表示する。
(どうやってタスクスレッドで発生した未処理例外を UI に通知すればよいのか?)
マルチスレッド処理を行う Windows フォームアプリケーションを書く上では、上記のような問題が発生したときの対処方法を知っておく必要があります。本エントリでは、これらについて解説します。
- タスクスレッドからの UI 画面の更新方法
- タスクスレッドからの UI 画面上のデータの読み取り方法
- UI 画面からのタスクスレッドの制御方法
- タスクスレッド上で発生した未処理例外の取り扱い方法
本エントリのサンプルコードはこちらになります。
では、以下に順に解説していきます。
[タスクスレッドからの UI 画面の更新方法]
さて、まずはおさらいです。タスクスレッド(マニュアルスレッドやプールスレッド)から UI 画面を更新する際には、タスクスレッドからコントロールのプロパティを直接いじってはいけません。このような場合には、BeginInvoke() 命令を利用し、メッセージキューに処理要求を投入する必要があります。具体的な作業手順は以下の通りです。
- UI 画面更新用の private メソッドを作成する。
画面更新に必要なデータをパラメータとして受け取るように書いておきます。(注意点は後述) - それにあわせた形で private デリゲートを定義します。
名称は任意ですが、××Delegate といった名前で定義しておくと便利です。 - タスクスレッドから BeginInvoke() 命令でメソッド呼び出しを実施する。
パラメータは object[] 配列として引き渡します。
なお、.BeginInvoke() 命令はどのコントロールに対して発行しても OK ですが、通常は親フォームオブジェクト(= this)に対して発行するのが便利です。
なお、関連する注意点として、以下についても知っておくと便利です。
① 現在のスレッドが UI スレッドかどうかは、InvokeRequired プロパティでチェックすることができる。
例えば上記の LongTask() メソッドは、マニュアルスレッド上だけでしか動作させられないかというとそんなことはなく、書き方や呼び方次第では、プールスレッド上や UI スレッド上で動作させることもできます。このため、あるメソッドが UI スレッド上で呼び出されることも、マニュアルスレッドやプールスレッド上で呼び出されることも、どちらもありうるような場合には、.InvokeRequired プロパティを確認することで、UI スレッド上で動作しているか否かを確認することができます。
ただし実際には、そもそも上記のようなチェックが必要とならないようにすることが望ましいです。ここまで取り扱ってきたサンプルは、必ず、あるメソッドはマニュアル/プール/UI のどれかの上でしか動作しないように設計してきました。実際、UI スレッドで行うべき処理とタスクスレッドで行うべき処理は異なっているのが当然で、UI スレッド/タスクスレッド両用になるようなメソッドというのは、(ユーティリティ的な処理を除けば)あまりないはずです。
UI フォームを設計・実装する際には、どのメソッドがどのスレッド上で動くのかを明確に意識しながら作業することが非常に重要です。InvokeRequired プロパティを使わなければならなくなった場合には、そもそもアプリケーションの設計としての是否をきちんと確認するようにしてください。
② 基本データ型及びイミュータブルオブジェクト以外を引き渡す場合には、オブジェクトの同期制御が必要になる。
UI スレッドからタスクスレッドを起こす場合でも、またタスクスレッドから UI スレッドへ制御を戻す場合でも、どちらでも共通する内容ですが、一般に、オブジェクトを引数として渡す場合には、参照渡しが行われます。例えば、下図のように、タスクスレッドから UI スレッドに StringBuilder の変数を引き渡すと、同一インスタンスがマニュアルスレッドと UI スレッドの両方から操作されることになります。
当然ですが、このような処理を行うと、同一インスタンスを同時に複数スレッドから操作することになり、データが破損することになります。よって、このようなコードは書いてはいけません。StringBuilder のインスタンスに対して同期制御を行うか、インスタンスをコピー(ディープコピー)して渡す必要があります。
このことからわかるように、UI スレッドとタスクスレッド間で、引数として基本データ型やイミュータブルオブジェクト以外のデータを引き渡す場合には、オブジェクトの同期制御が必要になります。(データ変数の同期制御に関しては、こちらとこちらのエントリに詳しく書いてありますので、よくわからないという方は確認してみてください。)
では引き続き、タスクスレッドからの UI 画面上のデータの読み取り方法について解説します。
[タスクスレッドからの UI 画面上のデータの読み取り方法]
上記では、タスクスレッドから UI 画面上へのデータ表示について考えたわけですが、実は、タスクスレッドから UI 画面上のデータを読み取ることも NG です。
UI コントロールを操作するためには、UI スレッドからの操作が必要になり、そのためにはメッセージキューへのメッセージ投入が必要....なのですが、ここまで解説してきた BeginInvoke() 命令では、メッセージを投入した後「やりっぱなし」の状態になってしまい、コントロールから値を読み取るなどした後の結果値を受け取ることができません。このような場合には、.Invoke() 命令を利用します。これを利用すると、UI スレッド上で行われた処理の結果を、タスクスレッド側で受け取ることができます。具体的な実装例を以下に示します。
なお、この .Invoke() 命令による、「UI スレッド上での処理結果の受け取り」はむやみに利用しないようにしてください。.BeginInvoke() 命令と異なり、.Invoke() 命令は、投入したメッセージが UI スレッド上で処理されるのを同期的に待つ(=タスクスレッド側は UI スレッド上での処理の終了を待機する)形になります。このため、以下のようなコードを書くとハングアップする危険性があります。
- 例1. Invoke() 命令で呼び出したメソッド内から、さらに別のスレッドを起こす ⇒ スレッドプールの枯渇や無限ループの発生
- 例2. 共有データに対して lock を取得している最中に Invoke() 命令を利用し、呼び出し先で当該データを操作 ⇒ デッドロックの発生
このようなことから、以下のルールを守ることをおすすめします。
- .Invoke() 命令は極力使わないようにする。
- 使う場合であっても、UI 上のプロパティを読み取ったらすぐに終了する、といった極力単純なコードのみを書くようにする。
- ロックを取得している最中に .Invoke() を呼び出さない。
では引き続き、UI 画面からのタスクスレッドの制御方法について解説します。
[UI 画面からのタスクスレッドの制御方法]
タスクスレッド上で時間を要する処理を起動した場合、UI からそのタスクを制御したいと思うことがよくあります。例えば、
- キャンセルボタンを押したら、背後で行っている処理を中断したい。
- チェックボックスやラジオボタンなどを押すと、背後で行っている処理が変更されるようにしたい。
といったことはしばしばあります。しかし、このような処理を作り込むのは意外に厄介です。なぜなら、UI からタスクスレッドで行われている処理に対して、割り込みをかけるような形で強制通知を行うことはできないからです。
例えば、以下のような画面を作る場合を考えてみます。
タスクスレッドでの処理を止めるため、キャンセルボタンを押したら、背後で行っている処理が中断されるように、Thread.Abort() 命令を使ったとします。この機能を使うと、タスクスレッドの処理を強制的に中断することができますが、そのスレッドがどんな状態であるかを全く無視して強制中断してしまうため、場合によっては共有データ変数の破損などの問題を引き起こすリスクがあります。このため、タスクスレッドへ強制的に割り込みをかけるような形で処理をキャンセルさせることはできないのです。
このような場合には、以下のような解決が必要になります。
- 状態を保存しておく共有データ変数を用意しておく。
- UI からタスクスレッドへ通知したい場合、共有データ変数に格納する。
- タスクスレッドが定期的に(=タスクの節目節目で)共有データ変数をチェックし、それに従った動作をする。
具体的な内部設計図を下図に示します。
実装コードを以下に示します。
コード上、特に注目していただきたいのが、共有データ領域の使い方です。UI スレッドから、強制的にタスクスレッドの処理に割り込みをかけることはできません。このため、button2_Click() メソッドでは、フラグ情報を立てておくだけにとどめます。一方、タスクスレッド側は、なるべく頻繁にこのフラグをチェックし、そのフラグに応じて処理をキャンセルしたり挙動を変更したりします。
このように、UI からタスクスレッドへ何かしらの通知を行う、というのは意外に実装が厄介なものである、ということを覚えておいてください。再度ポイントをまとめると、以下のようになります。
- 状態を保存しておく共有データ変数を用意しておく。
- UI からタスクスレッドへ通知したい場合、共有データ変数に格納する。
- タスクスレッドが定期的に(=タスクの節目節目で)共有データ変数をチェックし、それに従った動作をする。
なお、UI からタスクスレッドへの通知に関してもう一点注意していただきたいのが、Windows フォームのクローズ(アプリケーションの終了)に伴う動作です。Windows フォームの代表的なプロセス終了命令には、以下の 2 種類があり、それぞれ内部動作が異なります。
- Application.Exit() 命令
すべてのメッセージループを停止し、アプリケーションウィンドウを閉じ、メインスレッドの Application.Run() 命令を終了する。この際、フォアグラウンドスレッドが残留しているとプロセスが終了しない。 - Environment.Exit() 命令
強制的にプロセスを切り落とす。この際、フォアグラウンドスレッド、バックグラウンドスレッドの残留状態を問わず、プロセスが強制終了する。
後者の Environment.Exit() 命令はアプリケーションの強制終了に相当するものですから、通常、Windows フォームのアプリケーションを穏やかに終了させるには、前者の Application.Exit() 命令を利用します(この動作はフォームのクローズ時も同様です)。
しかし、Application.Exit() 命令でアプリケーションを終了させる際に問題になるのが、以下の 2 つです。
- フォアグラウンドスレッドが残っていると、プロセスが終了しない。
- バックグラウンドスレッドについては、強制終了となる。
ここまで、フォアグラウンドスレッドとバックグラウンドスレッドについての解説をしてきませんでしたので、まずこれについて解説します。.NET アプリケーションで取り扱う各スレッドには、フォアグラウンドスレッドか、バックグラウンドスレッドかのマーキングを行うプロパティ .IsBackground が存在します。そして、
- フォアグラウンドとマークされたすべてのスレッドが終了すると、CLR はバックグラウンドスレッドが残っていても当該プロセスを終了します。
- フォアグラウンドとマークされたスレッドが一つでも残っていると、Main() メソッドが終了しても CLR は当該プロセスを終了しません。
という挙動をします。例えば、下記のコンソールアプリケーションのサンプルを見てください。
このサンプルにおいて、
- バックグラウンド化した上でマニュアルスレッドを起動すると、Main() 関数が終了した直後にプロセスが終了します。
- しかし、バックグラウンド化せずに(t.IsBackground = false; として)マニュアルスレッドを起動すると、Main() 関数が終了しても、プロセスが終了しません。
両者の動作を以下に示します。
バックグラウンド化した場合
フォアグラウンドで動作させた場合
デフォルト状態での各スレッドの状態は、以下の通りです。
- Main() 関数を動作させるスレッド(メインスレッド) → フォアグラウンド
- マニュアルスレッド → フォアグラウンド
- プールスレッド → バックグラウンド
つまり、Windows フォームアプリケーションでマルチスレッドアプリを作成する場合、うかつにマニュアルスレッドを使うと、Application.Exit() 命令ではプロセスが終了しなくなります。
例えば、以下のようなアプリケーションを作ってみます。
※ ここでは、UI がなくなってもプロセスが残留することを簡単に示すために、DebugView を使います。このツールの詳細はここでは解説しませんが、簡単にいうと、System.Diagnostics.Debug.WriteLine() 命令によって出力したデータを、外部で簡単に参照できるようにしたツールです。Web アプリケーションなどの UI を持たないツールにおいて、内部動作を簡単に表示・モニタできるというメリットがあって便利です。ここからダウンロードできます。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: Thread t = new Thread(new ThreadStart(Task));
4: t.Start(); // バックグラウンド化せずに起動する
5: }
6:
7: private void Task()
8: {
9: while (true)
10: {
11: Thread.Sleep(300);
12: System.Diagnostics.Debug.WriteLine("タスク実行中...");
13: }
14: }
15:
16: private void button2_Click(object sender, EventArgs e)
17: {
18: Application.Exit();
19: }
フォアグラウンドでマニュアルスレッドを起動しておくと、フォームを閉じてもアプリケーションプロセスは終了せず、デバッガに情報を出力し続けます。(タスクマネージャを見ると、フォーム画面は消えているのにフォームの .exe プロセスが残っていることが確認できます。)
つまり、Windows フォームを適切に終了させるためには、残留しているフォアグラウンドスレッドをすべて終了させなければならない、ということになります。このことから、Windows フォームアプリケーションの開発に関する推奨事項として、次のようなことが言えます。
- 基本的に、タスクスレッドは「いつ強制終了されても困らない」ような防御的プログラミングをしておいた上で、さらにバッグラウンド化(.IsBackground = true)しておくことが望ましい。
「防御的プログラミング」というのは、「強制終了などのトラブルが起こったとしても、問題が起こらないようにプログラミングしておく」ことを指します。具体的には、以下のようなプログラミングの工夫を指します。
- ローカルキャッシュファイルが破損していた場合には、自動的に最新データをサーバから取り寄せる。
- ローカル設定ファイルが破損していた場合は、デフォルト値を利用する。
※ なお、このような防御的プログラミングの考え方は、Windows フォームアプリケーションでは特に重要です。というのも、Windows フォームアプリケーションは、「閉じる」ボタンなどで簡単に終了できるのはもちろんのこと、タスクマネージャからの強制終了や Windows OS シャットダウンなど、「完全な後片付け」ができないまま強制的に切り落とされるケースが多数考えられるからです。このため、Windows フォームアプリケーションでは『強制的に切り落とされても問題なく再起動ができる』形で実装することが望ましいといえます。タスクスレッド上の処理に限らず、「再起動時に何らかのローカルキャッシュデータファイルなどが破損していても自動復旧できる」ような設計をしておくことは非常に重要である、ということを覚えておいてください、とつぶやいておく^^。
では最後に、タスクスレッド上で発生した未処理例外の取り扱い方法について解説します。
[タスクスレッド上で発生した未処理例外の取り扱い方法]
一般的に、Windows フォーム上で発生した未処理例外に関しては、集約例外ハンドラを利用した後処理を行います。Windows フォームにおける集約例外ハンドラの書き方についてはこちらのエントリに記述しましたが、Application.ThreadException イベントハンドラで捕捉される例外は、メッセージループが捕捉した例外、すなわち UI スレッド上で発生した例外に限定されます。このため、マニュアルスレッドやタスクスレッド上で発生した未処理例外については、適切なハンドリングが必要です。これを怠ると、以下のような問題が発生します。
- タスクスレッドが例外によって終了してしまったにもかかわらず、タスクスレッドの正常終了を UI 側が永遠に待ち続けてしまう。
- タスクスレッド上で発生した例外情報がロギングされなかったため、障害解析ができなくなってしまう。
これらの問題を解決するため、以下のような対処を行います。
- AppDomain に対して集約例外ハンドラを仕掛けておく。
- タスクスレッド上で発生した例外を UI スレッド上でリスローする。
これらについて解説します。
AppDomain に対して集約例外ハンドラを仕掛けておく
CLR 上でアプリケーションを動作させる場合、プロセス内にはアプリケーションドメイン(AppDomain)と呼ばれる論理区画領域が作成されます。この AppDomain の UnhandledException イベントをフックすると、マニュアルスレッド上の未捕捉例外をすべて捕捉することができます。通常は、Application.ThreadException イベントハンドラも利用しますので、以下のようなコードを書くことになります。
ただし、この方法には以下の 2 つの難点があります。
- UI 通知ができない。 (タスクスレッド ≠ UI スレッドであるため)
- プールスレッド上で発生した例外を補足できない。
後者は特に大きな問題です。このため、実際には以下に述べる例外のリスローを使って、問題を解決することが望ましいです。これについて解説します。
タスクスレッド上で発生した例外を UI スレッド上でリスローする
そもそも UI 表示は UI スレッド上でしか行えない、という前提条件を考えると、タスクスレッドで発生した例外を UI 上で通知するためには、例外オブジェクトを UI スレッド側に伝搬する必要があります。そこで、タスクスレッド上で例外が発生した場合には、この例外オブジェクトを .BeginInvoke() 命令で UI スレッド側に伝搬し、UI スレッド上でそれをリスローします。
もともと UI スレッド上で発生する例外に関しては、Application.ThreadException イベントハンドラの集約例外ハンドラで捕捉することができますので、この方法を利用すれば、すべての例外を UI 側の集約例外ハンドラで補足することができます。
例えば、XML Web サービスの呼び出し処理をタスクスレッドに切り出す場合について考えてみます。XML Web サービス呼び出しに失敗すると、例外が発生します。このような例外をキャッチし、UI スレッドに伝搬すれば、集約例外ハンドラ(Application.ThreadException イベントハンドラ)でまとめて処理することができます。
# ちなみに、この場合の例外処理(try-catch)は、タスクスレッド上で発生したすべての例外をことごとく捕捉する必要がありますので、全体を大きく囲むことになります。この try-catch は、「例外→業務エラーの変換のための try-catch」でも「リソース解放のための try-finally」でもないので、注意してください。
[今回のエントリのまとめ]
というわけで、今回のエントリのキーポイントをまとめると、以下のようになります。
- タスクスレッドから UI 画面を更新したい場合には、.BeginInvoke() 命令を使って、メッセージキューにメッセージを投入する。
- タスクスレッドから UI 画面上のデータを読み取りたい場合には、データを読み取って返すメソッドを作成しておき、これを .Invoke() 命令でタスクスレッドから同期的に呼び出す。ただし気をつけてプログラミングしないと、スレッドプールの枯渇やデッドロック問題を引き起こすので注意が必要。
- UI 画面からタスクスレッドを直接操作することはできない。共有データ変数領域を用意しておき、① UI スレッドからはこの共有データ変数領域にフラグを立てる。② タスクスレッド側ではこの共有データ変数領域を頻繁にチェックする。という方法で間接的に制御を行う。
- タスクスレッド上で発生した未処理例外を取り扱うため、タスクスレッド全体を try-catch で囲む必要がある。捕捉した例外は、.BeginInvoke() 命令で UI スレッドに搬送し、リスローする。
さて、ここまで読んでみていただいて、
ちょーめんどい;。
と思われた方も多いと思います。はい、私もそう思います;;;。だったら説明するなー!と言われそうですが;、実際、マルチスレッドアプリケーションというのは、思っているよりも遥かに実装が厄介なものです。例えばボタンが押されたら XML Web サービスを呼び出すアプリケーションを作ろう、と思った場合、
同期型で実装する場合には、たったの 4 行で話が済みます。
しかし、これをタスクスレッドに切り出す場合、以下のようなポイントに対する考慮が必要になります。
- ボタンの二重押しの防止
- ステータスの表示
- タスクスレッドから UI スレッドへの書き戻し
- 集約例外処理
結果、以下のような膨大なコードになります。
なので、やはり個人的におすすめしたいのは、
マルチスレッドアプリケーションはなるべく作らない。
これに尽きます。って、まるでここまでの解説すべてを放り出すような発言ですが;;;、ここまでの延々とした説明をきちんと理解した上でコーディングをしようと思うと、相当大変であることは容易にご理解いただけるのではないでしょうか。(というか正直言ってこのエントリ書いてる自分もサクサク書けません、とても;。)
Visual Studio 2008 を利用すると、こうした問題を多少緩和するために、いくつかの機能が利用できるようになるのですが、そうはいってもここまでの解説をきちんと理解した上でないと、やはりそうしたウィザード類の利用もやはり危険です。Part 4. では Visual Studio 2008 の機能について解説しますが、Part 3. までの内容をよく理解した上で利用していただくことをお勧めします。
-
さて、前回のエントリでは、Windows フォーム内部におけるスレッドの構成や、メッセージループの働きなどについて解説しました。中でも重要なこととして、以下のようなキーポイントがありました。
- UI スレッド上で、長時間処理を動かしてはならない。
長時間処理は、マニュアルスレッドやプールスレッドなどの、他のスレッドに切り出す。 - UI スレッド以外から、UI コントロールを触ってはいけない。
マニュアルスレッドやプールスレッド上から、UI コントロールを読み書き・操作してはいけない。
上記の 2 つの重要ルールについて、Part 2~4 にてより実践的な解説を行っていきます。
- Part 2. タスクスレッドの起動方法
まず、マニュアルスレッドやスレッドプールの起動方法について解説します。 - Part 3. タスクスレッドと UI の協調動作
マニュアルスレッドやプールスレッドから UI コントロールを操作したり読み書きしたりすることはできないため、その回避方法について解説します。 - Part 4. Visual Studio を使ったマルチスレッドアプリケーション開発
上記 Part 2, 3 の作業を簡素化するために用意されている、Visual Studio の機能について解説します。
まず本エントリでは、UI スレッドから切り離した処理を動かすために利用するマニュアルスレッドやプールスレッドのことを、タスクスレッドと呼ぶことにし、その作成方法について解説します。
- デリゲートとは何か
- マニュアルスレッドの新規作成
- スレッドプールへのワークアイテムの追加
- 非同期デリゲートの利用
- タイマの利用
なお、以降の説明では様々なスレッドの起動方法を解説していきますが、突き詰めると、タスクを動かすスレッドには、マニュアルスレッドかプールスレッドかのどちらかを使っています。ただ、その起動方法が様々な種類がある、というだけの話ですので、見かけの多様性に惑わされず、しっかり学習していただければと思います。(今回はサンプルらしいサンプルはないですが、一応くっつけておきます。)
では、順番に解説していきます。
[デリゲートとは何か]
マニュアルスレッドやプールスレッドの起動処理を記述する上で欠かせない技術のひとつが、デリゲートです。デリゲートとは、オブジェクトに対して、「関数や処理ロジック」を引数として渡す際に利用される技術であり、.NET Framework の基盤技術の一つになっています。
そもそも「関数」や「処理」を引き渡すイメージがわかない、という方も多いと思いますので、まずここで簡単に解説します。以降の解説は、スレッディングの話からはちょっとそれますが、非常に重要なので必ず理解してください。(※ すでにデリゲートをご存じの方は、この項目は飛ばして先へ進んでください)
まず、なぜ「関数」を引数として引き渡す必要があるのかを理解するために、「コレクションから、ある条件を満たすものだけを抽出する処理」を考えてみることにします。例えば、List<int>型(動的配列)に含まれる整数値から、3で割り切れるものだけを取り出す処理を行う場合、おそらく多くの方は以下のようなコードを記述すると思います。
1: List<int> data1 = new List<int>();
2: for (int i = 0; i < 100; i++) data1.Add(i);
3:
4: // 自力抽出方式
5: List<int> data2 = new List<int>();
6: foreach (int i in data1)
7: {
8: if (i % 3 == 0)
9: {
10: data2.Add(i);
11: }
12: }
13:
14: foreach (int i in data2) Console.WriteLine(i);
この処理方式は、イラストであらわすと次のように示されます。
この方式は、「ユーザーがコレクション内のデータを1つずつ取り出しては吟味し、手作業で移し変えていく」ようなモデルです。もちろん、この処理自体は正しく動作するのですが、そもそも「何らかの条件に基づいてデータの抽出を行う」という処理自体、非常によく出てくる処理です。
そこで、以下のようなモデルを取ることができないか否かを考えてみます。
つまり、List<int> 型のコレクションに対して、「3で割り切れるか否かを確認するロジック」(=抽出条件のロジック)を外部から与えて、これに基づいて、コレクションクラス自身がデータを自動抽出するようなモデルで実装できないか、と考えるわけです。
実は、.NET Framework の「デリゲート」と呼ばれる仕組みを利用すると、これが実現できます。具体的には、以下の通りです。
- List<int> 型には .FindAll() というメソッドが備わっている。
- このメソッドには、引数として、関数(ロジック)そのものを渡すことができる。
実装方法を以下に示します。
- まず、CheckData() 関数(引き渡すことになる抽出条件判定関数)を作成しておく。
- これを「デリゲート」と呼ばれるオブジェクト(ここではPredicateオブジェクト)にラッピングして 、引数として .FindAll() メソッドに引き渡す。
このようにすると、List<int> 型コレクション(data1)自身が、渡されたロジックに基づいて抽出処理を行い、抽出結果を返します。
1: static void Main(string[] args)
2: {
3: List<int> data1 = new List<int>();
4: for (int i = 0; i < 100; i++) data1.Add(i);
5:
6: // デリゲート(関数ポインタ)方式
7: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
8:
9: foreach (int i in data2) Console.WriteLine(i);
10: }
11:
12: static bool CheckData(int i)
13: {
14: return (i % 3 == 0);
15: }
ここで重要なのは、以下のポイントです。
- 本来、引数として渡せるものは、文字列や数値といった「具体的なモノ」。
- しかし、デリゲートのインスタンスでラッピングすると、処理(=関数)を引き渡すことができる。
また、デリゲートで重要なもう一つのポイントは、あるデリゲートクラスがラッピングできる関数は、そのデリゲートが定義している引数/戻り値と完全に一致していなければならない、という点です。上記のサンプルの場合、Predicate<int> というデリゲートでラッピングできる関数は、int 引数ひとつを取り、bool 型を返すような関数に限られています。引数や戻り値の型などがひとつでもずれていると、そのデリゲートでのラッピングはできませんので、注意してください。
※ (注意) 上記のコードでは少し簡略化して書いていますが、デリゲートインスタンス(関数をラッピングしたもの)はそれ自体を変数によって保持することができます。例えば、上記のコードは以下のようにも書けます。(普通は面倒なのでまとめて一行で書いてしまいますが)
List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
↓
Predicate<int> d = new Predicate<int>(CheckData);
List<int> data2 = data1.FindAll(d);
※ (注意) .NET Framework 2.0 以降では、下記のように、デリゲートを使わずに、直接、関数名をメソッド引数として渡すことができるようなコードを書くことができるようになっています。しかし、これはコンパイラが自動的にコードを補正してくれるためで、内部的にはデリゲートにラッピングされたコードとしてコンパイルが行われます。この機能は実装時には便利なこともあるのですが、今回は簡略化コードを使わずに、きちんとデリゲートでラッピングしたコードを書いていくことにしたいと思います。
1: // .NET Framework 2.0 以降のコンパイラでは、下記のコードでもコンパイルが通るが...
2: List<int> data2 = data1.FindAll(CheckData);
3:
4: // 内部的には、下のようなコードに変換された上で、コンパイルされている
5: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
では引き続き、タスクスレッドの作成方法について順番に解説していきます。
[マニュアルスレッドの新規作成]
まず、最も基本となる、マニュアルスレッドの新規作成方法について解説します。マニュアルスレッドを新規で作成するには、System.Threading 名前空間にある、Thread クラスのインスタンスを作成し、これを起動します。基本的な実装方法は、以下の通りです。
- マニュアルスレッド上で動作させたい処理を、引数なし、戻り値なしのメソッドとして作成する。
- このメソッドを ThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引渡す。
- スレッドインスタンスの .Start() メソッドをたたくと、新規にマニュアルスレッドが起動し、引き渡しておいたメソッドが起動する。
コードサンプルを以下に示します。
※ (注意) スレッドを起動する前に、t.IsBackground = true; という設定をしていますが、この設定を行うとこのスレッドがバックグラウンドスレッドとしてマークされます。Windows フォームアプリケーションを終了する際に利用する Application.Exit() 命令は、「すべてのメッセージループを停止し、アプリケーションウィンドウを閉じ、メインスレッドの Application.Run() 命令を終了する」というものですが、フォアグラウンドスレッドが残留しているとプロセスが終了しません。このため、マニュアルスレッドは、バックグラウンドスレッド設定をしてから起動することが望ましいと言えます。
なお、この ThreadStart デリゲートは、System.Threading 名前空間の下側に定義されており、引数なし、戻り値が void 型のメソッドをラッピングすることができるデリゲートになっています。よって、この方法では、UI スレッドからマニュアルスレッドへとデータを直接引き渡すことができません。もちろん、上記のコードに示したように、共有変数領域を作成しておき、この領域を使ってデータを引き渡すこともできますが、この方法の場合、UI スレッドとマニュアルスレッドが同時にこのデータを操作する危険性があるため、排他制御が必要になります。(スレッド間での処理競合については、以前のエントリ(こちらとこちら)を参考にしてください。)
この問題を解決するために、.NET Framework 2.0 で導入されたのが、ParameterizedThreadStart デリゲートです。以下に具体的な実装方法を示します。
- マニュアルスレッド上で動作させたい処理を、引数 object 型ひとつ、戻り値なしのメソッドとして作成する。
- このメソッドを、ParameterizedThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引き渡す。
- スレッドインスタンスの .Start() メソッドに、object 型のパラメータを一つ渡して叩くと、新規にマニュアルスレッドが起動する。
この方法を利用すれば、明示的にデータ変数を引き渡すことができます。なお、この方法で認められている、UI スレッドからマニュアルスレッドへ引き渡せるパラメータは object 型変数 1 つだけですが、object 型ですのでなんでも渡すことが可能です。(複数のデータ項目を引き渡したい場合には、構造体にまとめたり object[] 配列などにして、これを引き渡せばよい) データを受け取ったメソッド側では、これを元のデータ型にキャストしてから利用してください。
では次に、スレッドプールの使い方について解説します。
[スレッドプールへのワークアイテムの追加]
スレッドプールは、マニュアルスレッドと異なり、自力でスレッドを新規に作成して利用するというものではありません。以前のエントリで解説したように、すでに起動しているスレッドに対して、メソッドを引き渡して処理してもらう、という形になります。
具体的には、以下の作業を行います。
- まず、引数として object 型変数を一つ、戻り値として void 型となるメソッドを用意する。
- これを WaitCallback デリゲートに包んで、プールのキューに追加する。(ThreadPool クラスの QueueUserWorkItem() メソッドを利用する)
実装コードサンプルは以下の通りです。
なお、スレッドプールのワークアイテムキューに追加できるデリゲートは、WaitCallback デリゲートのみになっています。WaitCallback デリゲートは、object 型引数ひとつ、void 型戻り値を持つメソッドしかラッピングできませんので、逆に言うと、スレッドプールによって非同期化できるメソッドのパラメータは object 型一つに限られる、ということになります。複数のパラメータを引き渡したい場合には、
- object 型の配列を使って一個の引数にまとめる。
- 構造体のようなクラスを使って一つの引数にまとめる。
- 後述する非同期デリゲートを使う。
のいずれかの方法を使う必要があります。
[ここまでのまとめ]
さて、ここまでの解説を一度まとめておきます。
- マニュアルスレッドを作成するためには、Thread クラスを作成する。
Thread クラスによりマニュアルスレッド上で起動できるメソッドは、引数なし/戻り値 void 型のメソッドか、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。
- プールスレッドを利用して処理を行わせるには、ThreadPool クラスを利用する。
ThreadPool クラスの .QueueUserWorkItem メソッドを使って、処理を投入する。投入できるメソッドは、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。
なお重要な注意点ですが、マニュアルスレッドやプールスレッド上で動作しているメソッドから、UI コントロールを読み書き・操作しては絶対にいけません。当然、マニュアルスレッドやプールスレッド上で長時間処理が終わったら、それをエンドユーザに通知したりする必要はあるのですが、そのためにうかつに button1.Enabled = true; とか label1.Text = “処理が完了しました”; とか MessageBox.Show(“Congulaturations!”); とか書いてはいけません。うっかりこれをやってしまうと、アプリケーションがクラッシュする危険性がありますので、十分に注意してください。
※ (参考) なお、マニュアルスレッドとプールスレッドの使い分けに関しては、Windows フォームアプリケーションの場合にはそれほどシビアになる必要はありません。スレッドプールのスレッドには上限があるため、スレッドプールが枯渇しないよう、長時間処理についてはマニュアルスレッドを使う、というのが一般的なベストプラクティスです。しかし多くの業務アプリケーションでは、そもそもそんなにたくさんの処理を同時に走らせるわけではありません。ASP.NET ランタイムなどでは内部的に大量のプールスレッドが利用されますが、Windows フォームでは、背後で走らせたいタスク処理はせいぜい数個程度でしょうから、プールスレッドを使ったところで枯渇現象を起こすことはないでしょう。このため、Windows フォームアプリケーションの場合には、長時間処理であってもプールスレッド上で動作させてしまうことがしばしばあります。
さて、ここまで解説してきた方法だと、マニュアルスレッドやプールスレッドに引き渡せるデータ変数にかなりの制限があることがわかると思います。もちろん object 型なのでなんでも渡せるといえばその通りなのですが、もうちょっと柔軟に引数として好きなものを渡す方法はないのか....と思うのも人情というものでしょう。実は、以下に解説する非同期デリゲートと呼ばれる機能を利用すると、複数の引数を持つメソッドを、プールスレッド上で処理することができるようになります。これについて解説します。
[非同期デリゲートの利用]
ここまでマニュアルスレッドやプールスレッドの使い方について解説してきましたが、これらに対して引き渡すために利用したデリゲートは、すべて .NET Framework 側で定義されているものであり、我々がデリゲートを定義することはしていませんでした。しかし、デリゲートは我々自身で定義することもできます。例えば、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラップするデリゲートは、以下のように定義することができます。
1: public delegate string MySampleDelegate(string a, int b);
このデリゲートを使うと、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラッピングしたオブジェクトを作ることができます。
1: // 以下のようなメソッドを作っておいて...(※ パラメタ名は一致していなくても OK)
2: public string MySampleMethod(string x, int y)
3: {
4: ... (何らかの処理) ...
5: }
6:
7: // このメソッドをラッピングしたデリゲートインスタンスを作る
8: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
9:
さて、このデリゲート(上の例では MySampleDelegate)のインスタンスには、.BeginInvoke() というメソッドが定義されています。この .BeginInvoke() メソッドを叩くと、デリゲートがラップしているメソッドに対する呼び出し要求が、スレッドプールのワークアイテムキューにキューイングされます。そしてその結果、ラップしているメソッドが、プールスレッド上で呼び出されることになります。
1: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
2: del.BeginInvoke("Nobuyuki", 123, null, null); // 後ろ 2 つのパラメータはコールバックに利用するもの(今回は解説しません。)
もう少し具体的な使い方として、Windows フォーム上で、非同期デリゲートを使って、ある長時間処理メソッドをプールスレッド上で動かすための手順を示すと、以下のようになります。
- プールスレッド上で実行したいメソッドを作成する。
このときのメソッドパラメータは任意ですが、戻り値は void としてください。
- 作成したメソッドの引数・戻り値に併せた形でデリゲートを定義する。
デリゲートの名称は、メソッド名 + Delegate とすると良いでしょう。(例: MethodX() に対して、MethodXDelegate とする) このデリゲートはここでしか利用しないので、public 宣言する必要はありません。private 宣言で十分です。
- UI スレッドからスレッドプールのワークアイテムキューにワークアイテムを投入する。
デリゲートインスタンスを作成し、BeginInvoke() 命令を発行します。これにより、当該メソッドへの呼び出しがワークアイテムキューに投入され、プールスレッドにより処理されます。(なお、呼び出しの際には、引数の末尾に null を二つつけてください。これらのパラメータは、コールバック処理や戻り値のハンドリングのために利用されますが、複雑なので今回は解説しません。)
具体的なコード例は以下の通りです。
さて、ここで解説したデリゲートが持っている .BeginInvoke() メソッドと、前回のエントリで解説した Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、名前こそ同じですが全く別物であることに注意してください。
- デリゲートが持っている .BeginInvoke() メソッドは、スレッドプールのワークアイテムキューへの投入である。
- Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、メッセージキューへのメッセージ投入である。
この違いをはっきりさせるため、以下のようなアプリケーション(ボタンを押すとプログレスバーが進んでいき、終了するとメッセージが表示される)を作成してみることにしましょう。

UI の内部設計図 は、以下の通りです。
具体的な実装方法は、以下のようになります。
UI スレッドからの、処理タスクの起動
- まず長時間を要する処理を、LongTask() メソッドとして定義します。ここでは例のため、名前と処理回数最大値を引数として取るようにしておくことにしましょう。
- 次に、LongTask() メソッドに対して、引数や戻り値を合わせたデリゲートを定義します。メソッドのすぐ上に定義しておくと都合がよいでしょう。名前は任意ですが、ここではメソッド名+Delegate という名前をつけることにします。
- ボタンが押されたら、デリゲートを使って、プールスレッド上でこの LongTask() メソッドを動作させるようにします。
プールスレッドからの UI の更新
- ここまで解説してきたように、プールスレッドから直接 UI を更新することはできません。そこで、UI を更新したい処理(プログレスバーへの表示とラベルへの表示処理)を、メソッドとして定義し(UpdateProgressBar(), UpdateLabel() メソッド)、それぞれに対してデリゲートを作っておきます。
- さらにメッセージキューへメッセージを投入するため、コントロールの .BeginInvoke() メソッドを使い、これらの処理を UI スレッド上で動作させます。

このように、UI スレッドとプールスレッドの連携協調動作には、デリゲートや .BeginInvoke() メソッド(2 種類)が利用されることを覚えておいてください。
なお、上記のサンプルでは、プールスレッドから UI スレッドへ処理を移す際に、this.BeginInvoke() メソッドを叩いていますが、この “this” はフォームそのもの(Form1)を示しています。実は、通常の Windows フォームアプリケーションでは、すべてのコントロールが同一の UI スレッドに属しており、そのような場合には、どのコントロールの .BeginInvoke() メソッドを叩いても同じ結果となります。ですので、上記のサンプル中の this.BeginInvoke() メソッドは、button1.BeginInvoke(), label1.BeginInvoke(), progressBar1.BeginInvoke() などと書いても同じ結果となります。
※ 参考(ちょっと難しいので、わからない人は読み飛ばしてください。)
デリゲートが持っている .BeginInvoke() メソッドを利用する場合は、本来のメソッド引数の後ろにさらに 2 つの引数を付与する必要があり、上記のサンプルでは null をつけていました。この 2 つの引数をうまく使うと、戻り値を持つメソッドへの呼び出しを非同期化したり、その結果を取り出したりすることができます。しかし、特殊な理由がなければ、Windows フォームのプログラミングではこの機能は利用する必要はありません。
例えば、非同期デリゲートを利用して、戻り値を持つメソッドへの呼び出しを非同期化する例を考えてみます。この場合には、以下のような設計と実装になります。
上記のコードを見てみると、確かに、後ろ 2 つのパラメータを使うことにより、プールスレッド上で開始した非同期処理の戻り値を受け取るメソッド(これをコールバック関数といいます)を作ることができます。しかし、この処理は UI スレッド上では動作していないため、結局、ここから UI の更新を行うことができません。結果として、上記のような面倒なコーディングが必要になってしまいます。これならいっそ、下に示すコードのように、コールバック関数を使わず、普通にメッセージキューへメッセージを投入するようなプログラムを書いた方が単純です。
このように、デリゲートが持つコールバック機能を利用すると、
- 戻り値を持ったメソッドへの呼び出しを非同期化する(プールスレッド上で動作させる)。
- その戻り値を、別のメソッド(コールバック関数)で受け取る。
ということが可能になるのですが、どちらかというと、コールバック関数を使わずに済ませるプログラミングの方が素直でしょう。(もともとコールバック関数は UI を持たない通常のマルチスレッドプログラミングで使うものなので、UI を持つアプリの場合には、コントロールの .BeginInvoke() だけを使った方が簡単なのですね^^。) なので、この機能については忘れてしまって OK です。
では、最後にちょっとした応用として、タイマの使い方について解説します。
[タイマの利用]
定期的に何らかのタスク処理を行いたい場合、タスクスレッドを起こしてそこでビジーループを作って待機することは、リソース利用上望ましくありません。むしろこのような場合には、.NET Framework 内に用意されているタイマオブジェクトを利用すると便利でしょう。
ただし注意したいのは、.NET Framework の中には 3 種類のタイマがあり、適切な使い分けが必要になる、という点です。具体的には、以下の 3 種類のタイマを使い分けていただく必要があります。(いずれも名称は “Timer” クラスですが、中身や機能は全くといっていいほど異なります。)
- System.Windows.Forms.Timer クラス (Windows アプリ向け)
定期的に Windows メッセージキューにメッセージを投入してくれるもの。
- System.Timers.Timer クラス (汎用タイマ)
定期的にスレッドプールのワークアイテムキューにワークアイテムを投入してくれるもの。
- System.Threading.Timer クラス (低水準タイマ)
低水準 API を提供するタイマ。1. や 2. の内部で使われている。
このうち、3. の System.Threading.Timer クラスは低水準タイマであるため、ほとんど使う必要はありません。基本的には 1. を中心に使い、場合によって 2. を併用する、という形になります。それぞれについて、具体的なコード例を示します。
1. System.Windows.Forms.Timer クラス (Windows アプリ向け)
まず、System.Windows.Forms.Timer クラスは、定期的な UI 更新処理に利用するタイマです。Windows フォームのツールボックス一覧に表示されている部品になりますので、画面に貼り付けて利用します。
利用する際は、timer1_Tick イベントハンドラの記述と、Interval プロパティへのタイマ発生間隔(msec)の設定を行います。このようにすると、System.Windows.Forms.Timer クラスは、定期的にメッセージキューにメッセージを投入してくれます。コード例と、内部動作の概念図を以下に示します。
1: private void timer1_Tick(object sender, System.EventArgs e)
2: {
3: // ここでは長時間処理は絶対にしないこと!(0.1sec ルール)
4: label1.Text = DateTime.Now.ToString(); // BeginInvoke()は不要
5: }
6:
7: private void button1_Click(object sender, EventArgs e)
8: {
9: timer1.Enabled = !timer1.Enabled;
10: }
この System.Windows.Forms.Timer コントロールは、メッセージキューに定期的にメッセージを投入するコントロールです。このため、動作上、以下の特徴や制約があります。
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、直接、UI を更新してよい。(.BeginInvoke() メソッドを使う必要はない。)
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、長時間処理をしてはならない。
よって、この Timer コントロールは、時計の表示などの単純な画面更新タスクの非同期化に利用するのが都合がよいでしょう。
2. System.Timers.Timer クラス (汎用タイマ)
次に、System.Timers.Timer クラスについて解説します。こちらは、定期的な業務処理を行うために利用するタイマで、スレッドプールのワークアイテムキューにワークアイテムを定期的に投入してくれるものです。
先ほどの Windows フォームの System.Windows.Forms.Timer クラスとは異なり、こちらは画面に貼り付けて利用する部品(コントロール)ではなく、通常のオブジェクトになります。実装例を以下に示します。
この System.Timers.Timer クラスのタイマーには、以下のような特徴があります。
- スレッドプールのワークアイテムキューに定期的に処理を投入する。
- UI スレッドをブロックしないため、時間のかかる処理も実施できる。
- 半面、UI 更新のためには BeginInvoke() を利用する必要がある。
特に、最後のポイントについては注意してください。このタイマーのイベントハンドラ(上記のコード例の場合には t_Elapsed() メソッド)は、プールスレッド上で動作しますので、直接、UI コントロールを操作してはいけません。必ず、UI コントロールの BeginInvoke() メソッドにより、UI スレッドへの処理投入を行う必要があります。
(参考&応用) なお、少し裏ワザ的な機能になりますが、System.Timers.Timer クラスの Synchronized プロパティを使うと、t_Elapsed イベントハンドラを UI スレッド上で動作させることができます。しかし、この機能を使うぐらいなら最初から System.Windows.Forms.Timer コントロールを使った方がラクなので、そちらをお勧めします。
3. System.Threading.Timer クラス (低水準タイマ)
上記 2 つのタイマは、それぞれ
- 定期的な UI 更新タスクを動かしたい → System.Windows.Forms.Timer コントロール
- 定期的な業務処理を動かしたい → System.Timers.Timer クラス
というように使い分けますが、これらの内部で低水準 API として利用されているのが、ここで解説するSystem.Threading.Timer クラスになります。ただし、こちらは低水準 API であるため、基本的に使いません。参考までに実装例を以下に示しますが、通常は使わない、ということを覚えておいてください。
以上で解説した 3 種類のタイマの使い分け・比較をすると、以下のようになります。実際に利用するのは、1. と 2. のタイマである、ということを覚えておいていただければと思います。
[今回のエントリのまとめ]
というわけで、ここまで様々なタスクスレッドの起動方法について解説してきましたが、それぞれのタスクスレッドの起動方法には様々なトレードオフがあります。
- 起動パラメータ引渡し可否
- 動作スレッドの種類
- 記述できる処理の長さ
- UI オブジェクト操作時のスレッド同期要否、etc…
これらを比較表としてまとめると、以下のようになります。
もちろん、いずれも一長一短があるわけですが、基本的には以下のように使い分けるとよいでしょう。
- 通常のタスクスレッドの起動には、非同期デリゲートを使う。
非同期デリゲートは最も汎用性が高く、制限が少ないためです。
- 定期的な UI 更新処理については、System.Windows.Forms.Timer コントロールを使う。
ただし、イベントハンドラでは XML Web サービス呼び出しなどの長時間処理は行ってはいけません。
- 定期的な業務処理については、System.Timers.Timer クラスを使う。
ただし、イベントハンドラでは UI を直接操作してはいけません。
- プールの枯渇を考えなければならないような場合は、マニュアルスレッドの利用を検討する。
タスクスレッドの起動については、実装コードを見て「なるほど」と思っても、実際に自分でプログラミングしてみると意外に手詰まりしてしまうことが多いです。今回示したサンプルコードを実際に一度手を動かして組んでみると、なるほどと納得できるところも多いと思いますので、ぜひ一度トライしてみてください。
-
さて、Windows フォームは、Windows OS が持つ様々なウィンドウ制御の仕組みに基づいて開発されている UI 技術です。このため、Windows フォームのマルチスレッド処理を理解するためには、まず Windows OS がどのようにして Windows フォームアプリケーションを動作させているのかについて理解する必要があります。その中でも特に重要なのが、メッセージキューとメッセージループです。これらを理解することで、なぜ UI が固まるのか、また固まることを防ぐにはどうしたらよいのか、といったことが理解できるようになります。これについて解説します。
- メッセージキューとメッセージループ
- UI フリーズの発生理由
- Windows フォーム上でのマルチスレッド処理の基本ルール
- BeginInvoke() 命令
- 最も簡単なマルチスレッドアプリケーションの例
- Windows フォームにおけるスレッドの種類
なお、今回のサンプルは以下の通りです。ご活用ください。
[メッセージキューとメッセージループ]
Windows フォームは、メッセージループと呼ばれる仕組みを使うことにより、イベント駆動型のプログラミングモデルを実現しています。まず、概略図を以下に示します。

エンドユーザがマウスやキーボードによって Windows フォームのアプリケーションを操作した際に Button_Click などのイベントが発生するのは、以下のようなメカニズムによります。
- マウスやキーボードからの入力は、まず Windows OS が受け取る。
- Windows OS は、その操作内容(キーが押された、マウスが動いた、マウスのボタンがクリックされた、etc)を、メッセージ構造体(MSG 構造体)に固め、それを各アプリケーション用のメッセージキューに放り込む。
- 各アプリケーション内部では、メッセージループと呼ばれる処理が走っている。
- メッセージループは、自分用のメッセージキューからメッセージ構造体をひとつずつ取り出し、そのデータを解析し、イベントハンドラ呼び出し(Button_Click 呼び出しなど)を行う。
※ なお、ここでいうメッセージキューとは、MSMQ (Microsoft Message Queue)のことではありません。Windows OS が持っている、GUI 処理のための特殊なキューです。
C# で Windows フォームのアプリケーションを記述した方であれば、Main 関数の中に、以下のような Application.Run() 命令を記述したことがあると思います。この命令は、メインスレッド上でメッセージループを起動するためのものです。(VB だと内部的にこの処理が隠ぺいされるためこのコードが見えませんが、内部的には同じ処理が行われています。)
このメッセージループには、以下のような特徴があることを覚えておいてください。
- メッセージループは、一種の無限ループです。つまり、メッセージキューからメッセージを取り出して処理し、次のメッセージを取り出して処理し、...をひたすら繰り返します。メッセージがなくなると、次のメッセージが届くまで待機しますが、いずれにしてもこのメッセージループは終了しません。メッセージループのコードは .NET Framework 内部に実装されているため見ることができませんが、コードイメージとして、以下のような処理が行われていると思っていただけるとわかりやすいでしょう。
while (true)
{
メッセージを取り出す処理(); // (取りだせなかった場合は待機する)
メッセージの内容を解析する処理();
メッセージの内容に基づいて、イベントハンドラを呼び出したりする処理();
} - メッセージループから、(開発者が記述した)イベントハンドラが呼び出されるまでの流れは、スタックトレースを見てみるとわかります。
さて、このメッセージループによるメッセージの取り出しにおいて重要なことは、メッセージの取り出し作業がシングルスレッド処理である、という点です。つまり、
- ひとつのメッセージを取り出して、イベントハンドラ処理(Button_Click 処理など)を行っている最中は、次のメッセージが取り出されることはありません。
ということになります。実はこれが、UI フリーズが発生する主な原因になります。
[UI フリーズの発生理由]
では次に、UI のフリーズ(UI が固まって操作できなくなる現象)がなぜ発生するのかについて解説します。先ほど、メッセージキューに OS が投入する代表的なメッセージとして、以下のようなものを挙げました。
- キーが押された
- マウスが動いた
- マウスのボタンがクリックされた
しかし、実は OS が投入するメッセージには、これ以外にも次のようなものがあります。
例えば画面上で、最小化されていたフォームがタスクバーからクリックされ、非アクティブだったフォームがアクティブ化されたとします。この場合、OS は、当該アプリに対して「UI を描画しなさい」という命令(メッセージ)を、(メッセージキューを介して)送ります。これを受け取った Windows フォームアプリは自分を描画することで、フォームを表示することになります。
つまり、メッセージキューに投入されるメッセージの中には、Windows OS からの再描画要求やサイズ変更要求などもあります。こうしたメッセージをすぐに処理できないと、UI が固まったり、正しくウィンドウが表示されなくなったりするように見える、ということになるわけです。
ところが先ほど述べたように、メッセージループによるメッセージキューからのメッセージの取り出しは、ひとつずつ順次行われます。このため、メッセージループを持つスレッド上で時間のかかる処理を行ってしまうと、再描画処理が即座に行われず、UI がフリーズします。
例えば、以下のようなコードを書いたとします。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: System.Threading.Thread.Sleep(5000);
4: }
このようなコードを書いて実行すると、ボタン押下中はウィンドウがうんともすんとも言わなくなり(=ウィンドウを移動することなどができなくなり)、UI がフリーズします。理由は簡単です。
- メッセージループを動作させているメインスレッドが、button1_click() を処理している最中は、次のメッセージを処理できない。
- このため、OS からウィンドウの移動や再描画要求メッセージが送られても、このアプリケーションはそれに反応できない。
つまり、
- Windows フォームのイベントハンドラ(Button_Click や TextBox_TextChanged など)は、メッセージループから同期的に呼び出される。
- これらの中で時間のかかる処理を行ってしまうと、簡単に UI がフリーズする。
ということになります。言い換えれば、
- UI を全くフリーズさせないためには、メッセージループから呼び出されるイベントハンドラで、時間のかかる処理(具体的には 0.1 sec 以上かかる処理)を行わないようにすればよい。
ということになります。
なお、イベントハンドラ内では何秒程度までの処理なら認められるのか? については、なんとも言い難いものがあります。一般的に、人間の視覚速度は 30fps (秒間 30 フレーム、1 フレームあたり約 30msec)と言われており、これ以下であればスムーズに動作しているように見える、と言われています。とはいえ 30msec はかなり厳しい制限です。現実的には、各イベントハンドラ内の処理を 3 フレーム程度、つまり 100msec 程度以内に収まるように設計・実装すれば、ほとんどフリーズが感じられない、応答性の高いアプリになります。(もっとも、これはアプリの特性などによっても変わりますので、一概に言える数字ではありませんが。)
逆に言えば、次のようなことがいえます。
- UI がフリーズしないアプリを作るためには、時間のかかる処理(具体的には 0.1sec 以上かかる処理)を別スレッドに切り離して実行しなければならない。
業務アプリケーションには、多かれ少なかれ、こうした「時間のかかる処理」があります。典型的なものとしてはネットワークアクセス、具体的にはデータベースアクセスや XML Web サービスへのアクセスがあります。こうした時間のかかる処理をうかつにイベントハンドラに記述してしまうと、メッセージループをブロックし、UI がフリーズすることになります。今回のエントリで解説するのは、このような処理をいかにして別スレッドに切り出すのか、についてです。
では引き続き、別スレッドへの処理の切り出し方の具体的な説明を....と言いたいところですが、その前にもうひとつ説明すべきことがあります。それは、Windows フォーム上てのマルチスレッド処理に関する基本ルールです。
[Windows フォーム上でのマルチスレッド処理の基本ルール]
マルチスレッド処理を行う Windows フォームアプリケーションを開発する際には、必ず以下のルールを守る必要があります。
- 親子関係を持つコントロールは、必ず同一スレッドに所属させること。
Windows フォームのコントロール(UI 部品)は、インスタンス生成時に、それが生成されたスレッドに自動的に紐付けられるように設計されています。このため、フォーム上にテキストボックスやラベルなどがある場合、それらはすべてフォームのインスタンスを生成したスレッドと同一スレッド上で生成しなければなりません。
通常は、すべての UI 部品をメインスレッド上で生成し、ここでメッセージループを起動します。(このため、メインスレッドは UI スレッドとも呼ばれます。以降の解説は、すべてこの前提条件に基づいて解説します。)
- コントロールを生成したスレッド以外から、コントロールを操作しないこと。
Windows フォームのコントロールは、スレッドセーフではありません。このため、当該コントロールを作成したスレッド(通常は UI スレッド)以外から直接プロパティなどを操作してはいけません。
特に後者は非常に重要です。Windows フォーム内でバックグラウンドタスクを実行するために背後のスレッドを起動した場合、そこから UI 上に進捗状況を表示したり、処理結果を表示したりしたいことが多々あります。しかし、このような際に、背後のスレッドから直接 label1.Text = “…(処理結果)…”; といった具合に直接コントロールを操作すると、最悪の場合、アプリケーションがクラッシュします。
この問題を解決するために用意されているのが、BeginInvoke() 命令です。
[BeginInvoke() 命令]
先に述べたように、UI 部品はそれを作成したスレッド、通常は UI スレッド(=メッセージループを動作させているスレッド)から操作しなければなりません。では、上図のように独自に起動した処理スレッドから UI を更新したい場合にはどのようにすればよいかというと、BeginInvoke() 命令を利用します。この命令は、すべての Windows フォームコントロールが備えているメソッドで、簡単にいうと、「特定のメソッドを呼び出せ」という命令を、メッセージ構造体としてメッセージキューに投入するためのものです。
具体例を出しながら解説した方がわかりやすいと思いますので、一例として、以下のような画面(バックグラウンドで処理を進めている際に、進捗状況を ProgressBar に表示する)を考えてみます。
まず、バックグラウンドスレッドから、直接 ProgressBar を操作するのは厳禁です。つまり、progressBar1.Value = 87; といった具合に、ProgressBar コントロールを UI 以外のスレッドから直接操作する(上図の青いスレッドから操作する)ことは厳禁です。これを避けるために、以下の作業を行います。
- まず、画面更新用のメソッドをコードビハインド中に作成します。
private void UpdateProgressBar(int val)
{
progressBar1.Value = val;
}
- 次に、このメソッドのポインタ情報をラップするための、デリゲートクラスを定義します。(UpdateProgressBar メソッドのすぐ上に、UpdateProgressBarDelegate などの名前で配置するとわかりやすいです。なおデリゲートの詳細は、Part 2 にて解説します。)
private delegate void UpdateProgressBarDelegate(int val);
private void UpdateProgressBar(int val)
{
progressBar1.Value = val;
}
- 最後に、背後で動作するバックグラウンドタスクのスレッドから、BeginInvoke() 命令を利用してメッセージを投入します。(コードの全体像は後で示します。なお、通常はすべてのコントロールが UI スレッドに所属しているため、どのコントロールの BeginInvoke() 命令を利用しても同じ結果となります。通常は Form クラスの BeginInvoke() 命令を叩くとよいでしょう。)
// 進捗状態として画面を更新
this.BeginInvoke(new UpdateProgressBarDelegate(UpdateProgressBar),
new object[] {i});
このようにすると、バックグラウンドタスクのスレッドから、メッセージキューに UpdateProgressBar() メソッドを呼び出すメッセージが投入され、これが処理されると画面が更新されます。BeginInvoke() 経由で投入されたメッセージに含まれるメソッド呼び出しは、UI スレッド上で処理されることになるので、このメソッドでは安全に UI を更新することができます。
[最も簡単なマルチスレッドアプリケーションの例]
では上記のコードサンプルを利用して、以下のような「進捗表示アプリケーション」を作ってみることにしましょう。
具体的な作業手順は以下の通りです。まず、フォーム上にボタンとプログレスバーを貼り付けます。
次に、ボタンのクリックイベントハンドラに以下のようなコードを書き、実行してみてください。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: button1.Enabled = false; // いったんボタンを無効化
4: for (int i = 0; i < 100; i++)
5: {
6: // 何らかのタスクを実施...
7: Thread.Sleep(100);
8: // 進捗状態として画面を更新
9: progressBar1.Value = i;
10: }
11: button1.Enabled = true; // 再びボタンを有効化
12: }
実際に実行してみると、(プログレスバーの表示は一部きちんと動いてくれるものの)ウィンドウを動かそうとすると UI がフリーズしてしまったりします。これは、この button1_Click() メソッドが UI スレッド(メッセージループの処理)を占有してしまっており、OS からの再描画要求に応答できなくなってしまっているためです。
このような形になるのを避けるためには、この処理をバックグラウンドのスレッドに切り離す必要があります。バックグラウンドのスレッドとしては、プールスレッドとマニュアルスレッドの 2 種類がありますが、比較的長時間を要する処理(ここに示したような数秒以上かかるような処理)については、マニュアルスレッドを使う方がよいでしょう。まず、上記の for ループ処理をバックグラウンド処理に切り離します。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: button1.Enabled = false; // いったんボタンを無効化
4: Thread t = new Thread(new ThreadStart(LongTask));
5: t.IsBackground = true; // バックグラウンド化してから起動
6: t.Start();
7: }
8:
9: private void LongTask()
10: {
11: for (int i = 0; i <= 100; i++)
12: {
13: // 何らかのタスクを実施...
14: System.Threading.Thread.Sleep(100);
15:
16: // 進捗状態として画面を更新
17: progressBar1.Value = i;
18: }
19: button1.Enabled = true; // 再びボタンを有効化
20: }
上記のコードの 17 , 19 行目に着目してください。
- button1_Click() は、メッセージループから呼び出されます。つまり UI スレッド上で動作します。
- しかし、上記に示した LongTask() は、新規に作成されたマニュアルスレッド上で動作します。
つまり、この 17 行目や 19 行目のコードは、UI スレッド以外からコントロールを操作しているので、アプリケーションをクラッシュさせる危険性があります。この問題を避けるためには、17 行目や 19 行目の処理を UI スレッド上で動作させるように、BeginInvoke() 命令を使う必要があります。
具体的には、まず UI 更新を行うためのメソッドと、そのメソッド呼び出しをラッピングするためのデリゲートを定義します。
1: private delegate void UpdateProgressBarDelegate(int val);
2: private void UpdateProgressBar(int val)
3: {
4: progressBar1.Value = val;
5: if (val == 100) button1.Enabled = true;
6: }
次に、LongTask() メソッド内の UI 更新処理を、BeginInvoke() によるメッセージ投入処理に切り替えます。
1: private void LongTask()
2: {
3: for (int i = 0; i <= 100; i++)
4: {
5: // 何らかのタスクを実施...
6: System.Threading.Thread.Sleep(100);
7:
8: // 進捗状態として画面を更新
9: this.BeginInvoke(
10: new UpdateProgressBarDelegate(UpdateProgressBar),
11: new object[] { i });
12: }
13: }
以上により、フリーズしない UI を持った進捗状況表示画面が作成されます。
完成したソースコードと、それぞれのメソッドがどこで動作するのかを下図に示します。
このようにすることで、時間のかかる処理を背後のスレッドに分離し、フリーズしない UI を作成することができます。
[Windows フォームにおけるスレッドの種類]
さて、上記では処理の切り離しにマニュアルスレッド(新規に作成したスレッド)を使いましたが、スレッドプールを使って処理を別スレッド化することもあります。(※ スレッドプールがどのようなものであるかについては、以前に記述したエントリを参照してください。)
つまり、Windows フォームでは、UI スレッド(メインスレッド)から処理を切り離す方法として、マニュアルスレッドとプールスレッドの 2 種類がある、ということになります。結果として、Windows フォームアプリケーション内部では、主に以下のようなスレッドが利用されることになります。
- メインスレッド (UI スレッド)
当該プロセス内に最初に作られ、Main() メソッドを呼び出すスレッド。このスレッド上で UI コントロールを作成し、Application.Run() メソッドを呼び出し、メッセージループを動作させる。メッセージキュー内のメッセージ処理や、各種のイベントハンドラ呼び出しはこのスレッド上で発生する。
- マニュアルスレッド
非同期処理(バックグラウンドタスク)を行うために、自力で Thread オブジェクトを生成することにより作ったスレッド。
- プールスレッド
非同期処理(バックグラウンドタスク)を行う際、CLR の機能であるスレッドプール機能を用いる場合に利用されるスレッド。
- その他のスレッド
ファイナライザスレッドやアンマネージスレッドなどが動作するスレッド。通常は気にしなくて OK。
まとめると、下図のようになります。注意すべき点は、マニュアルスレッドやプールスレッドから UI コントロールを直接更新してはならないという点です。UI を更新したい場合には、BeginInvoke() 命令を使って、メッセージキューにメソッド呼び出し要求を投入してください。
[今回のエントリのまとめ]
というわけで、今回のエントリでは Windows フォームのマルチスレッド処理の基礎について解説してきました。キーポイントは以下の通りです。
- Windows OS は、メッセージキューにメッセージ(MSG 構造体)を投入することによって、マウスの移動やキーボードの押下を通知している。
- Windows フォームアプリケーションは、内部でメッセージループを使い、メッセージキューから一件ずつ、逐次でメッセージを取り出していくことで処理を進める。
- UI フリーズが発生する原因は、メッセージループ上(UI スレッド上)で長時間処理を行ってしまうことである。イベントハンドラなどに長時間処理を記述すると、OS からの再描画要求が実行されなくなり、UI がフリーズする。
- 通常、UI コントロールはすべてメインスレッド上でインスタンス化し、このスレッド上から操作する。このメインスレッドのことを、別名で UI スレッドと呼ぶ。
- マニュアルスレッドやプールスレッドといった、メイン以外のスレッドから UI コントロールを操作してはならない。このような場合には、BeginInvoke() 命令を使って、UI スレッド上で画面描画更新処理を動作させる。
以上が基本的な Windows フォームのマルチスレッド処理の大原則です。しかしこれだけではまだマルチスレッド動作する Windows フォームアプリケーションを開発するにはやや知識が足りません。次回のエントリでは、上記の大原則に従った、より詳細なマルチスレッドアプリの開発方法について解説してきます。
-
というわけでまたしてもかなり日にちが空いてしまいました;。年度末ということもあって仕事が立て込んでいたのですが、ほぼ一段落したので久しぶりにエントリを。どうしてもまとまった話題を書こうとすると時間がかかっちゃいますね....
今回の話題は、Windows フォームにおけるマルチスレッド処理の正しい書き方です。以前、マルチスレッドアプリケーションにおけるデータ変数の排他制御(スレッドセーフか否かの判定)についてこちらとこちらのエントリに書きましたが、さらにもう少し応用的なトピックとして、Windows フォームにおけるマルチスレッド処理について解説したいと思います。
この辺の話は、C++ でアプリケーションを作られている方には当たり前の話(らしい)のですが、私のような pure .NET デベロッパーな人にはあまり馴染みのない話だと思います。しかし、C# や VB でしか Windows フォームのアプリケーションを書いたことがない人であっても、うまくコードを書かないと、下図のような「フリーズしてしまう」アプリケーションができあがってしまいます。
今回のエントリでは、以下について解説します。
- そもそもなぜ UI のフリーズ現象は発生するのか? それはどうすれば回避できるのか?
- Windows フォームにおける UI スレッドの位置づけ
- Windows フォーム上で、背後にスレッドを起こすための適切な方法
では、以降のエントリでこれらについて解説していきます。
[Part 1. Windows フォームのマルチスレッド処理の基礎]
- メッセージキューとメッセージループ
- UI フリーズの発生理由
- Windows フォーム上でのマルチスレッド処理の基本ルール
- BeginInvoke() 命令
- 最も簡単なマルチスレッドアプリケーション
- Windows フォームにおけるスレッドの種類
[Part 2. タスクスレッドの起動方法]
- マニュアルスレッドの新規作成
- スレッドプールへのワークアイテムの追加
- 非同期デリゲートの利用
- タイマの利用
[Part 3. タスクスレッドと UI の協調動作]
- タスクスレッドからの UI 画面の更新方法
- タスクスレッドからの UI 画面上のデータの読み取り方法
- UI 画面からのタスクスレッドの制御方法
- タスクスレッド上で発生した未処理例外の取り扱い方法
[Part 4. Visual Studio によるマルチスレッドアプリの開発]
- XML Web サービス呼び出しの非同期処理化
- WCF サービス呼び出しの非同期処理化
- BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化