2015/08/26 初版
.NET Framework 4.5(C#5.0)(Visual Studio 2012)からasync/awaitという構文が追加されました。
この構文は非同期な並列処理を簡単にコーディングする事ができます。
async/awaitを使った非同期処理(マルチスレッド)についてまとめてみようと思います。
非同期処理(マルチスレッド)でないとどうなるのか
以下のコードはスタートボタンが押されると重たい処理を実行し、キャンセルボタンでその処理を停止させようとした例ですがうまくいきません。
同期的な処理の例
public partial class Form1 : Form { private bool m_Cancel; public Form1() { InitializeComponent(); } //ボタンを押された時に重たい処理をする //直列処理の場合 private void btnStart_Click(object sender, EventArgs e) { this.btnStart.Enabled = false; this.btnCancel.Enabled = true; m_Cancel = false; for (int i = 1; i <= 10; ++i) { //キャンセルの処理 //他の処理が動かない為m_Cancelがtrueになる事は無い if (m_Cancel) break; //1秒に1回TextBoxへ表示する処理 //他の処理が動かない為TextBoxの表示も変わらず完全に固まる this.textboxProc1.Text = "処理:(" + i + "/10)"; Thread.Sleep(1000); } this.btnCancel.Enabled = false; this.btnStart.Enabled = true; } private void btnCancel_Click(object sender, EventArgs e) { m_Cancel = true; } }
スタートボタンがクリックされると処理が開始されます。
しかし処理が実行している最中にはボタン操作が一切効きません。処理の中で進捗状況をテキストボックスへ表示させようとしていますが(22行目)それも反映されません。
全ての処理が直列に実行されるため、処理が終わるまでボタンをクリックしたりテキストボックスの表示を書き換えたりといった処理は動作しないことになります。
同期的な処理の例 (DoEventsあり)
public partial class Form1 : Form { private bool m_Cancel; public Form1() { InitializeComponent(); } //ボタンを押された時に重たい処理をする //直列処理の場合(DoEvents有り) private void btnStart_Click(object sender, EventArgs e) { this.btnStart.Enabled = false; this.btnCancel.Enabled = true; m_Cancel = false; for (int i = 1; i <= 10; ++i) { //キャンセルの処理 //DoEvents()によりキャンセルは可能に if (m_Cancel) break; //1秒に1回TextBoxへ表示する処理 //DoEvents()によりTextBoxの表示が変わるように this.textboxProc1.Text = "処理:(" + i + "/10)"; Thread.Sleep(1000); //DoEvents()により他のウインドウメッセージが処理される //但し、1秒に毎にしか処理されないので動きがぎこちない //また、ウインドウを移動中の時などでは、ここの処理が止まる Application.DoEvents(); } this.btnCancel.Enabled = false; this.btnStart.Enabled = true; } private void btnCancel_Click(object sender, EventArgs e) { //DoEvents()によりキャンセルボタンのクリックが可能に //但し、1秒に毎にしか処理されないので動きがぎこちない m_Cancel = true; } }
最初の例にApplication.DoEventsメソッドを追加してみました。
DoEventsメソッドはメッセージキューにある全てのウインドウズメッセージを実行します。
これにより、メッセージキューにあるボタンクリックやテキストボックスの更新といったものが重たい処理の間に実行されるようになります。
とは言えあくまで同期的な処理です。上記の例では1秒に1回しかDoEventsが呼ばれないのでマウス操作などがぎこちない動きになってしまいます。
非同期処理(マルチスレッド)にする
以下の例はasync/await構文を使って重たい処理の部分を非同期で実行するようになっています。
非同期的な処理の例
public partial class Form1 : Form { private CancellationTokenSource m_CancelToken; public Form1() { InitializeComponent(); } //ボタンを押された時に重たい処理をする //async awaitによる並列処理 //メソッドにasyncを付ける事でawaitが利用可能になる private async void btnStart_Click(object sender, EventArgs e) { this.btnStart.Enabled = false; this.btnCancel.Enabled = true; //キャンセル処理にはCancellationTokenSourceを使う m_CancelToken = new CancellationTokenSource(); //awaitで並列処理化 //並列処理部分を匿名メソッドで定義する await Task.Run(() => { for (int i = 1; i <= 10; ++i) { if (m_CancelToken.IsCancellationRequested) break; //非同期で処理されるので直接UIにアクセスできない //Invokeをつかって処理する this.Invoke((Action)(() => { this.textboxProc1.Text = "処理:(" + i + "/10)"; })); Thread.Sleep(1000); } }); //awaitで並列処理部分の処理を待つが、その間他の処理がちゃんと動く m_CancelToken.Dispose(); m_CancelToken = null; this.btnCancel.Enabled = false; this.btnStart.Enabled = true; } private void btnCancel_Click(object sender, EventArgs e) { //awaitで並列処理部分の処理を待つ間でもここに来る if (null != m_CancelToken) { m_CancelToken.Cancel(); } } }
重たい処理の部分は、非同期処理の為のTaskクラスを使って匿名メソッドになっています。(21行目)
このTaskオブジェクトをawait構文によって別スレッドによって非同期に実行しつつTaskの処理が終了するまで待機します。この間メッセージキューに溜まった処理は随時実行されていきます。
awaitを使うメソッドにはasyncを書くのがルールです。(12行目)
このようにasync/awaitを使う事で、コードの見た目は直列的に見やすく書きながら別スレッドで非同期に処理を実行するという事が可能になります。
おまけ
非同期的な処理の例 (複数処理を並列実行)
public partial class Form1 : Form { private CancellationTokenSource m_CancelToken; public Form1() { InitializeComponent(); } //ボタンを押された時に重たい処理をする //async awaitによる並列処理 //メソッドにasyncを付ける事でawaitが利用可能になる private async void btnStart_Click(object sender, EventArgs e) { this.btnStart.Enabled = false; this.btnCancel.Enabled = true; //キャンセル処理にはCancellationTokenSourceを使う m_CancelToken = new CancellationTokenSource(); //awaitで並列処理化 //並列処理部分を匿名メソッドで定義する await Task.Run(() => { Task task1 = HevyProc(this.textboxProc1); Task task2 = HevyProc(this.textboxProc2); task1.Wait(); task2.Wait(); this.Invoke((Action)(() => { this.textboxProc1.Text = "result:" + task1.Result; this.textboxProc2.Text = "result:" + task2.Result; })); }); //awaitで並列処理部分の処理を待つが、その間他の処理が適切に動く m_CancelToken.Dispose(); m_CancelToken = null; this.btnCancel.Enabled = false; this.btnStart.Enabled = true; } private void btnCancel_Click(object sender, EventArgs e) { //awaitで並列処理部分の処理を待つ間でもここに来る if (null != m_CancelToken) { m_CancelToken.Cancel(); } } private Task HevyProc(TextBox textbox) { return Task.Run(() => { for (int i = 1; i <= 10; ++i) { // キャンセル要求チェック if (m_CancelToken.IsCancellationRequested) return 1; //非同期で処理されるので直接UIにアクセスできない //Invokeをつかって処理する this.Invoke((Action)(() => { textbox.Text = "処理:(" + i + "/10)"; })); Thread.Sleep(1000); } return 0; }); } }
関連記事
- C#の値型と参照型の違い
- C#のコンストラクタでオーバーロード
- C#のコンストラクタの継承
- C#のジェネリックを使おう
- C#のデリゲート (delegate) って何?
- C#のデリゲートお手軽にする匿名メソッド
- C#のラムダ式【=>】って何?
- C#で基底クラスのメソッドを置き換えるオーバーライド
- C#でキャストとas演算子を使いこなす
- C#で型を判別するtypeofとis演算子
- C#の値型でもnullを扱えるようにするNullable
- C#のリソース解放にはIDisposableとusingを使おう
- C#のStringとstring、Int32とint 違いは・・・ない!
- C#でasync/awaitを使った非同期処理
- C#で文字列を指定の区切り文字で分割
- C#のstring.Formatで桁数や書式を指定する
- C#の配列やListをソートする
- C#の配列やListを検索する (Find,FindAll,FindIndex)
- C#の配列やListを高速に検索する (BinarySearch)
- C#の配列の中に指定の要素が存在するかを調べる(LINQ Contains)
- C#の配列の中に条件を満たす要素が存在するかを調べる(LINQ Any)
- C#の配列から条件に合う要素を抽出する(LINQ Where)
- C#の配列で要素毎の処理結果を得る(LINQ Select)
- C#の配列を並び替える(LINQ OrderBy,ThenBy)
- C#の配列をグループ毎に処理する(LINQ GroupBy)
- C#の配列を内部結合(INNER JOIN)する(LINQ Join)
- C#の配列から最初の要素を取り出す(LINQ First,FirstOrDefault)
- C#の配列の重複要素を削除する(LINQ Distinct)
- C#でフォルダ内のファイル名一覧を取得する
- C#でテキストファイルを読み込む
- C#でテキストファイルに書き込む
- C#でバイナリファイルを読み込む
- C#でバイナリファイルに書き込む
コメントをお書きください
かきそふと (水曜日, 11 4月 2018 22:39)
非常に参考になる記事をありがとうございます!
タイプミスを1つ見つけたので、お伝えしておきます。
「非同期的な処理の例」の、btnCancel_Clickメソッドにて、以下のように修正して実行しました。
『if (null != m_CancelSouce) 』⇒『if (null != m_CancelToken)』
AraramiStudio (木曜日, 12 4月 2018 09:35)
かきそふとさん
コメントありがとうございます。
本文中の該当箇所を修正しました。
ご指摘ありがとうございました。