C#のTaskの使い方がちょっと分かってきたので、備忘録として。
Taskを覚え始めた頃は、実行したい個所に、コピペのように下記の形式で書いていた。
var result = await Task<string[]>.Run(() =>
{
return Directory.GetFiles(@"C:\");
}
同じ処理を何か所にも書くのがきつくなって、下記のように、Taskを1回定義して、
実行したい個所で呼び出そうとした。(動きません。)
//Taskの定義
static Task<string[]> GetFilesTask = new Task<string[]>(() => Directory.GetFiles(@"C:\"));
//呼び出し側(このままではTaskは開始しない。)
public static async Task Main(string[] args)
{
var result = await GetFilesTask;
foreach (var item in result)
{
Console.WriteLine(item);
}
}
上記の書き方では、結果は出力されない。
理由は、awaitで結果待ちしているGetFilesTaskは、定義されているが開始されていないから。
結果、awaitが永遠に待ち続ける。
なので、下記のように開始してからawaitで結果待ちする。
static Task<string[]> GetFilesTask = new Task<string[]>(() => Directory.GetFiles(@"C:\"));
static async Task Main(string[] args)
{
//開始してからawaitで結果待ちする。
GetFilesTask.Start();
var result = await GetFilesTask;
foreach (var item in result)
{
Console.WriteLine(item);
}
}
次に、タイムアウトを設定したくなった。
(つながらないネットワークドライブにアクセスしようとすると、エラーになるまで時間がかかるから)
方法は、
・ファイルを取得するタスク
・指定時間経過後に終了するタスク
の2つを定義して、どちらか一方が完了したら、もう一方をキャンセルする。
キャンセルするには、CancellationTokenSourceを使って、タスクにキャンセル指示を出す。
ただし、下記の例で要注意なのは、GetFilesTaskが1000ミリ秒以内に完了して、かつ、
ディレクトリが見つからなかった場合、GetFilesTaskが、ネットワークパスが見つからないというIOExceptionをthrowするので、catchする必要がある。
//cts1.Cancel()メソッドを実行すると、cts1.Tokenを引数に持つ全タスクに取り消しを要求出来る。
private static CancellationTokenSource cts1 = new CancellationTokenSource();
private static Task<string[]> GetFilesTask = new Task<string[]>(() =>
Directory.GetFiles(@"\\192.168.99.99\DATA$"),cts1.Token);
static async Task Main(string[] args)
{
GetFilesTask.Start();
try
{
if(await Task.WhenAny(GetFilesTask, Task.Delay(1000, cts1.Token)) == GetFilesTask)
{
//ファイルを取得するタスクが先に完了すると、こっちが実行される。
//実行中のTsk.Delay()がcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("GetFilesTask完了");
}
else
{
//タイムアウトのタスクが先に完了すると、こっちが実行される。
//実行中のGetFilesTsakがcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("Task.Delay完了");
}
}
catch
{
//適当
throw;
}
}
ここまでは、GetFilesの引数を直入力していたが、引数を指定したくなった。
結果、メソッドの中で上記処理を行うようにした。
static async Task Main(string[] args)
{
CancellationTokenSource cts1 = new CancellationTokenSource();
try
{
var result = await GetFilesTask2(@"\\192.168.99.99\DATA$", 1000, cts1);
}
catch
{
//例外処理
}
}
private static async Task<string[]> GetFilesTask2(string directory, int timeoutMilliSeconds, CancellationTokenSource cts1)
{
var getFilesTask = new Task<string[]>(() => Directory.GetFiles(directory),cts1.Token);
getFilesTask.Start();
try
{
if (await Task.WhenAny(getFilesTask, Task.Delay(timeoutMilliSeconds, cts1.Token)) == getFilesTask)
{
//ファイルを取得するタスクが先に完了すると、こっちが実行される。
//実行中のTsk.Delay()がcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("getFilesTask完了");
return await getFilesTask;
}
else
{
//タイムアウトのタスクが先に完了すると、こっちが実行される。
//実行中のgetFilesTsakがcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("Task.Delay完了");
return null;
}
}
catch
{
//適当
throw;
}
}
cts1をこのメソッド内だけで使うなら、
CancellationTokenSourceもローカルでよいという事で下記に変更。
private static async Task<string[]> GetFilesTask2(string directory, int timeoutMIlliSeconds)
{
CancellationTokenSource cts1 = new CancellationTokenSource();
var getFilesTask = new Task<string[]>(() => Directory.GetFiles(directory),cts1.Token);
getFilesTask.Start();
try
{
if (await Task.WhenAny(getFilesTask, Task.Delay(timeoutMIlliSeconds, cts1.Token)) == getFilesTask)
{
//ファイルを取得するタスクが先に完了すると、こっちが実行される。
//実行中のTsk.Delay()がcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("OK");
return await getFilesTask;
}
else
{
//タイムアウトのタスクが先に完了すると、こっちが実行される。
//実行中のgetFilesTsakがcts1.Tokenを引数として持っているので、キャンセルされる。
cts1.Cancel();
Console.WriteLine("NG");
return null;
}
}
catch
{
//適当
throw;
}
}
個人的な注意点として、ファイルをコピーするタスクや変更するタスクの場合、
タイムアウトでキャンセルされると、ファイルが中途半端な状態になってしまう可能性がある。
Taskとは関係ないし当たり前の事だけど、処理結果の確認はしっかりしよう。
例えば1分くらいでタイムアウトさせたら、通信状態が悪くて想定外の未完了状態になってたり。