web-dev-qa-db-ja.com

Task.Factory.StartNew()は、呼び出し元のスレッドとは別のスレッドを使用することが保証されていますか?

関数から新しいタスクを開始していますが、同じスレッドで実行したくありません。別のスレッドである限り、どのスレッドで実行されるかは気にしません(したがって、 この質問 で提供される情報は役に立ちません)。

Task tが再び入力できるようにする前に、以下のコードが常にTestLockを終了することが保証されていますか?そうでない場合、再入を防止するための推奨設計パターンは何ですか?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}

編集:Jon SkeetとStephen Toubによる以下の回答に基づいて、再入を決定論的に防止する簡単な方法は、CancellationTokenを渡すことです拡張方法:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
73
Erwin Mayer

この質問について、 PFXチーム のメンバーであるStephen Toubにメールを送りました。彼はすぐに戻ってきて、多くの詳細を説明してくれたので、ここに彼のテキストをコピーして貼り付けます。引用された大量のテキストを読むと、Vanillaの白黒に比べて快適にならないため、すべてを引用していませんが、実際、これはStephenです-私はこれほど多くのものを知りません:)この回答コミュニティwikiは、以下のすべての良さが実際には私のコンテンツではないことを反映しています。

完了した TaskWait() を呼び出した場合、ブロッキングは発生しません(スローするだけです)タスクがRanToCompletion以外の TaskStatus で完了した場合、または nop )として返される場合は例外。既に実行中のタスクでWait()を呼び出す場合、合理的に実行できることは他にないのでブロックする必要があります(ブロックと言うときは、真のカーネルベースの待機とスピンの両方を含めます。通常、両方の混合を行います)。同様に、CreatedまたはWaitingForActivationステータスのタスクでWait()を呼び出すと、タスクが完了するまでブロックされます。それらのどれも議論されている興味深いケースではありません。

興味深いケースは、WaitingToRun状態のタスクでWait()を呼び出すときです。つまり、以前に TaskScheduler にキューに入れられていたが、TaskSchedulerは 'まだタスクのデリゲートを実際に実行することにまだ慣れていません。その場合、Waitへの呼び出しは、スケジューラのTryExecuteTaskInlineメソッドへの呼び出しを介して、現在のスレッドでその時点でタスクを実行してもよいかどうかをスケジューラに尋ねます。これはinliningと呼ばれます。スケジューラーは、base.TryExecuteTaskの呼び出しを介してタスクをインライン化するか、タスクを実行していないことを示すために 'false'を返すかを選択できます(多くの場合、次のようなロジックで行われます...

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);

TryExecuteTaskがブール値を返すのは、同期を処理して、指定されたタスクが一度だけ実行されるようにするためです。したがって、スケジューラがWait中にタスクのインライン化を完全に禁止したい場合は、return false;として実装できます。スケジューラが可能な場合は常にインライン化を許可する場合は、次のように実装できます。

return TryExecuteTask(task);

現在の実装(.NET 4と.NET 4.5の両方、および私は個人的にこれが変更されるとは思わない)では、ThreadPoolを対象とするデフォルトのスケジューラーは、現在のスレッドがThreadPoolスレッドであり、そのスレッドが以前にタスクをキューに入れたもの。

デフォルトのスケジューラーはタスクを待機しているときに任意のスレッドをポンプしないため、ここでは任意の再入可能性がないことに注意してください...そのタスクのインライン化のみが許可され、もちろんそのタスクのインライン化が決定されますする。また、Waitは特定の条件でスケジューラーに問い合わせることさえせず、代わりにブロックすることを好みます。たとえば、キャンセル可能な CancellationToken を渡した場合、または無限のタイムアウトを渡した場合、任意の長い時間がかかる可能性があるため、インライン化は試行されません。タスクの実行をインラインで実行します。これは、すべてまたはゼロであり、キャンセル要求またはタイムアウトを大幅に遅延させる可能性があります。全体として、TPLはWait 'ingを行っているスレッドを無駄にすることと、そのスレッドを過度に再利用することとの間で適切なバランスをとろうとします。この種のインライン化は、複数のタスクを生成し、それらがすべて完了するのを待つ再帰的な分割統治問題(例: QuickSort )にとって非常に重要です。インライン化せずにこれを行うと、プール内のすべてのスレッドと、将来提供するスレッドをすべて使い果たしてしまうため、非常に迅速にデッドロックに陥ります。

Waitとは別に、 Task.Factory.StartNew 呼び出しは、使用されているスケジューラが実行することを選択した場合に、その場でタスクを実行することも(リモートで)可能です。 QueueTask呼び出しの一部としてタスクを同期的に実行します。 .NETに組み込まれたスケジューラーはどれもこれを行うことはなく、個人的にはスケジューラーの設計としては悪いと思いますが、理論的には可能です、例えば:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}

TaskSchedulerを受け入れないTask.Factory.StartNewのオーバーロードは、TaskFactoryからのスケジューラーを使用します。これは、Task.Factoryの場合はTaskScheduler.Currentをターゲットにします。つまり、この神話上のRunSynchronouslyTaskSchedulerにキューイングされたタスク内からTask.Factory.StartNewを呼び出すと、RunSynchronouslyTaskSchedulerにもキューイングされ、StartNew呼び出しがタスクを同期的に実行します。これにまったく不安がある場合(たとえば、ライブラリを実装していて、どこから呼び出されるのかわからない場合)、TaskScheduler.DefaultStartNew呼び出しに明示的に渡し、Task.Runを使用できます。 (常にTaskScheduler.Defaultに移動します)、またはTaskScheduler.Defaultをターゲットにするために作成されたTaskFactoryを使用します。


編集:さて、私は完全に間違っていたように見え、現在タスクを待っているスレッドcanが乗っ取られます。この出来事の簡単な例を次に示します。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

サンプル出力:

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4

ご覧のとおり、待機中のスレッドが新しいタスクを実行するために再利用される場合が多くあります。これは、スレッドがロックを取得した場合でも発生する可能性があります。厄介な再入。私は適切にショックを受けて心配しています:(

83
Jon Skeet

なぜそれが起こらないように後方に曲がるのではなく、単にそれのために設計しないのですか?

TPLはここでは赤いニシンであり、サイクルを作成できるコードであれば再入可能です。スタックフレームの「南」で何が起こるかはわかりません。同期リエントラントはここでの最良の結果です-少なくともあなたは(簡単に)自己デッドロックすることはできません。

ロックは、スレッド間の同期を管理します。再入可能性の管理とは直交しています。純粋な使い捨てリソース(おそらく物理デバイス、おそらくキューを使用する必要があります)を保護している場合を除き、インスタンスの状態が一貫していることを確認して、再入可能性が「機能する」ようにしてください。

(側の考え:セマフォは減少せずに再入可能ですか?)

4
piers7

Erwinによって提案されたnew CancellationToken()を使用したソリューションは、私にとってはうまくいきませんでした。

だから私は、ジョンとステファン(... or if you pass in a non-infinite timeout ...):

  Task<TResult> task = Task.Run(func);
  task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start
  return task.Result;

注:簡単にするためにここでは例外処理などを省略していますが、本番コードではそれらに注意する必要があります。

0

スレッド/タスク間でソケット接続を共有するクイックアプリを作成することで、これを簡単にテストできます。

タスクは、ソケットにメッセージを送信して応答を待機する前にロックを取得します。これがブロックされてアイドルになると(IOBlock)、同じブロックに別のタスクを設定して同じことを行います。ロックの取得時にブロックする必要があります。ロックしない場合、同じスレッドで実行されるために2番目のタスクがロックを渡すことが許可されている場合、問題が発生します。

0
JonPen