1. ホーム
  2. c#

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

2023-10-24 11:56:03

質問

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

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

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());
}

どのように解決するのですか?

私は Stephen Toub にメールを送りました。 PFX チーム - にメールを送りました。彼は実に迅速に、そして多くの詳細とともに返信してくれました。大量の引用文を読むと、白黒のバニラよりも快適でなくなってしまうので、全部は引用していませんが、本当に、これは Stephen です - 私はこれほどのことは知りません:) 私はこの回答をコミュニティ wiki にして、以下のすべての良さは実際には私のコンテンツではないことを反映させました。

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

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

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

理由 TryExecuteTask がブール値を返すのは、与えられたタスクが一度しか実行されないことを保証するために同期を処理するためです。) したがって、スケジューラが、タスクのインライン化を完全に禁止したい場合 Wait の間にタスクのインライン化を完全に禁止したい場合、それは単に return false; もしスケジューラが可能な限り常にインライン化を許可したい場合は、次のように実装すればよいでしょう。

return TryExecuteTask(task);

現在の実装(.NET 4 と .NET 4.5 の両方、そして個人的にはこれが変わるとは思っていません)では、ThreadPool をターゲットとするデフォルトのスケジューラは、現在のスレッドが ThreadPool のスレッドであり、そのスレッドが以前にタスクをキューに入れたものであればインライン化を可能にします。

デフォルトのスケジューラーは、タスクを待っているときに任意のスレッドをポンピングしないという点で、ここには任意のリエントランシーがないことに注意してください...それはそのタスクがインライン化されることだけを許可し、もちろんそのタスクが今度は行うことを決定したインライン化も許可します。 また、以下の点にも注意してください。 Wait は特定の条件下ではスケジューラに問い合わせることすらせず、 代わりにブロックすることを好みます。 例えば、キャンセル可能な キャンセル・トークン を渡した場合、あるいは、無限でないタイムアウトを渡した場合、タスクの実行をインライン化するのに恣意的に長い時間がかかり、オール・オア・ナッシングとなり、キャンセル要求やタイムアウトを大幅に遅らせることになりかねないので、インライン化を試みないでしょう。 全体として、TPLは、タスクの実行を行うスレッドを無駄にすることとの間で、適切なバランスを取ろうと試みています。 Wait 'ingとそのスレッドを再利用しすぎています。 この種のインライン化は、再帰的な分割統治問題(例えば クイックソート など) で、複数のタスクを生成し、それらがすべて完了するのを待つような再帰的な分割統治問題では、この種のインライン化は本当に重要です。インライン化せずにこのような処理を行った場合、プール内のすべてのスレッドと、将来提供されるスレッドを使い果たすため、非常に迅速にデッドロックが発生します。

から分離された Wait を使うことも(リモートで)可能です。 タスク.ファクトリー.スタートニュー の呼び出しは、使用されるスケジューラが QueueTask 呼び出しの一部としてタスクを同期的に実行することを選択した場合、その場でタスクを実行してしまう可能性があります。 .NET に組み込まれているスケジューラーのどれもこれを行うことはなく、個人的にはスケジューラーとしては悪い設計だと思いますが、理論的には可能です。

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

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


EDIT: なるほど、完全に私の勘違いだったようで、現在タスク待ちのスレッドが を乗っ取ることができます。これが起こるより簡単な例です。

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

ご覧の通り、待機中のスレッドが新しいタスクの実行に再利用されることがたくさんあります。これはスレッドがロックを獲得している場合でも起こり得ます。不愉快なリエントランシー。それなりにショックで心配です :(