C#で同期メソッド-から非同期メソッドを呼び出すと、アプリケーションがフリーズする現象について紹介します。
以下のサンプルでアプリケーションがフリーズする状態を確認します。
下図のUIを作成します。
以下のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CallAsyncMethodFromSyncMethod
{
public partial class Form_Main : Form
{
public Form_Main()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Task<int> ret = AsyncTask();
ret.Wait();
textBox1.Text += Convert.ToString(ret.Result);
}
public async Task<int> AsyncTask()
{
Func<int> asyncJob = () =>
{
// 時間のかかる処理
int i = 0;
for (i = 0; i < 1000000000; i++) {
}
return i;
};
int ret = await Task.Run(asyncJob);
return ret;
}
}
}
プロジェクトを実行します。下図のウィンドウが表示されます。
[button1]をクリックします。デッドロックになり操作ができなくなることが確認できます。
ボタンのクリックイベントからAsyncTask()メソッドが呼び出され、AsyncTask()メソッド内の
int ret = await Task.Run(asyncJob);
で新しいタスクが実行されます。
awaitでタスクの完了まで以降の行は実行されず待機しますが、UIスレッド側では、
ret.Wait(); が呼び出され、スレッドが待ち状態(ロック状態)になります。Task側でawait Task.Run()を抜けて次の
return ret;
を実行しようとするにも、UIスレッドがロックされているため実行できず、すべてのメソッドが待ち状態になるため、デッドロックになります。
以下の対策があります。
Wait()メソッドでUIスレッドがTaskの終了待ち状態になっているため、
int ret = await Task.Run(asyncJob);
の後の
return ret;
をUIスレッドでないスレッドで実行できれば、タスクが終了でき、UIスレッドのWait()を抜けることができ、デッドロックを防げます。await後の処理を呼び出し元のコンテキスト(スレッド)とは異なるコンテキストで呼び出すには、ConfigureAwait()メソッドを呼び出し、引数(continueOnCapturedContext)にfalseを与えます。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CallAsyncMethodFromSyncMethod
{
public partial class Form_Main : Form
{
public Form_Main()
{
InitializeComponent();
}
private void button3_Click(object sender, EventArgs e)
{
Task<int> ret = AsyncTask2();
ret.Wait();
textBox1.Text += Convert.ToString(ret.Result);
}
public async Task<int> AsyncTask2()
{
Func<int> asyncJob = () =>
{
// 時間のかかる処理事
int i = 0;
for (i = 0; i < 1000000000; i++) {
}
return i;
};
int ret = await Task.Run(asyncJob).ConfigureAwait(continueOnCapturedContext:false);
return ret;
}
}
}
プロジェクトを実行します。下図のウィンドウが表示されます。
[Button3]をクリックします。アプリケーションはフリーズせずに動作します。タスクにより計算された結果もテキストボックスに表示されます。
Waitで待たずに、Task内と同じようにawait でタスクの完了を待つことで、待ち状態になることを防ぎます。awaitで待った場合はスレッドが待機状態にはなりません。
この方法を用いる場合は、ButtonのClickイベントのメソッドに asyncを記述して、非同期メソッドにする必要があります。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CallAsyncMethodFromSyncMethod
{
public partial class Form_Main : Form
{
public Form_Main()
{
InitializeComponent();
}
//async がついていることに注意
private async void button2_Click(object sender, EventArgs e)
{
int ret = await AsyncTask();
textBox1.Text += Convert.ToString(ret);
}
public async Task<int> AsyncTask()
{
Func<int> asyncJob = () =>
{
// 時間のかかる処理事
int i = 0;
for (i = 0; i < 1000000000; i++) {
}
return i;
};
int ret = await Task.Run(asyncJob);
return ret;
}
}
}
プロジェクトを実行します。下図のウィンドウが表示されます。
[Button2]をクリックします。アプリケーションはフリーズせずに動作します。タスクにより計算された結果もテキストボックスに表示されます。