Task.Run と Task.Factory.StartNew

Task.RunとTask.Factory.StartNewの比較表
項目Task
.Run
Task
.Factory
.StartNew
戻り値備考
指定可能な引数CancellationToken-キャンセルに使うTokenを指定
TaskCreationOptions×-どのように作成するかを指定するオプション
TaskScheduler×-同期コンテキストを
指定
第1引数ActionTask
Func
<TResult>
Task
<TReuslt>
Action
<object>
×Task第2引数に
Ojbectが必須
Func
<Object,TResult>
×Task
<TReuslt>
Func
<Task>
×Task入れ子のTaskの
内側が戻る
Func
<Task<TResult>>
×Task
<TReuslt>
使い勝手がいいTask.Run

比較表のとおり、Task.RunはTask.Factory.StartNewに比べ、設定できる項目が少ない。というか、Taskを作るデリゲートの他には、CancellationTokenしかない。

MSDNのブログで次の説明があった。

Task.Run(someAction);

that’s exactly equivalent to:

Task.Factory.StartNew(someAction,
 CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

Task.Run vs Task.Factory.StartNew | Parallel Programming with .NET

つまり、Task.RunはTask.Factory.StartNewで次のように設定したときと同じ。

設定項目説明
CancellationTokenNoneCancellationTokenを渡さない(この項目はTask.Runでも変更可能)
TaskCreationOptionsDenyChildAttach子タスクを親タスクにアタッチさせない
TaskSchedulerDefaultタスクスケジューラのデフォルト(スレッドプールで実行)

普通の用途では問題のない設定になっている。この設定を変えたい場合はTask.Factory.StartNewを使う必要があるけど、自分の場合、そういう状況がめったにない。

上記ブログによると、Task.Facotry.StartNewを一般的な用途でもっと手早く使うためにTask.Runが作られたようだ。確かに、タイプ数が少ないし使い勝手が良い。自分も多用している。

ただ、Task.RunのTaskCreationOptionが「DenyChildAttach」なので、Task.Runの中でTask.Factory.StartNewを「AttachedToParent」で使っても無効になってしまうことには注意が必要。

また、Task.Runは手軽だけど細かい設定はできない。Task.Factory.StartNewが不要になったわけではない。

非同期メソッドを想定したTask.Runのオーバーロード

次に、それぞれに渡す第1引数について見る。第1引数にはタスクで行う処理の中味となるデリゲートを渡すことになっている。

Task.Run、Task.Factory.StartNewともに「Action」と「Func<TResult>」というデリゲートの基本の型を引数にとることができる。

基本形に加え、Task.Factory.StartNewの方では引数を一つだけ取るデリゲート「Action<object>」「Func<object, TResult>」が用意されている。(引数が2つ以上のを使いたかったら?たぶん「ラムダ式でくるんで、入れられる型にしてね(はぁと」ということなのだと思う。でも、それならそもそもActionとFunc<TResult>だけでいいいんじゃ…とも思うし、よくわからない……。)

Task.Runの方は、「Func<Task>」と「Func<Task<TResult>>」が加わっている。意図は明白で(というか、さっきのブログに書いてたから知ったんだけど)非同期メソッドへの対応だ。

非同期メソッドの戻り値は前に書いた通り「void」「Task」「Task<T>」の3種。voidはActionで対応できるし、残り二つの戻り値に対応したFuncが追加されているわけだ。(Funcに引数がある場合はどうしたら……ってやっぱり「ラムダ式で(ry」なのだと思う。)

『非同期メソッドに対応ったって、Func<Task>もFunc<Task<TResult>>もFunc<TResult>で受けることもできるんだから、意味ないじゃん』と思うなかれ。比較表の戻り値をご覧じろ。

非同期メソッドを入れてTaskを作ると「入れ子のTask」になる。非同期メソッドから返されるTaskが、コンストラクタの作るTaskにくるまれるからだ。

TaskFactory.StartNewの場合、自分が作った入れ子のTaskをそのまま返してくる。が、Task.Newは自分で作ったTaskの包装は取って(UnWrap)非同期タスクから返されたTaskを渡してくれる

たとえば、Task<int>を返す非同期メソッドをTaskFactory.StartNewに与える場合の戻り値は

Task<Task<int>> task = Task.Factory.StartNew(asyncTaskInt);

Task<T>ではな<Task<Task<T>>となる。(まぁ、これが普通だ。)

が、Task.Runの場合

Task<int> task = Task.Run(asyncTaskInt);

Task<int>でいい。

便利だ!!でもこれ、知らないとハマることがある。だって、普通にTaskをnewしたときと型が変わっちゃうということだから…(実体験)。

まとめ
  • Task.Runは手軽で便利だ!
    • Task.Factory.StartNewを一般的な設定で使うのと同じ。
  • Taks.Runは戻り値も便利だ!
    • Task<TResult>の包みをほどいてTResultを渡してくれる。

参考ページ