76

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

以下のコードは、再度入力する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();
}

編集: JonSkeetとStephenToubによる以下の回答に基づいて、再入可能性を決定論的に防ぐ簡単な方法は、この拡張メソッドに示されているように、CancellationTokenを渡すことです。

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

4 に答える 4

83

この質問について、 PFXチームのメンバーであるStephenToubにメールを送りました。彼は非常に迅速に、詳細を含めて私に戻ってきたので、ここに彼のテキストをコピーして貼り付けます。引用されたテキストを大量に読むと、バニラの白地に黒よりも快適でなくなるため、すべてを引用していませんが、実際には、これはスティーブンです-私はこれほど多くのものを知りません:)私は作りましたこの回答コミュニティウィキは、以下のすべての長所が実際には私のコンテンツではないことを反映しています。

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

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

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

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

return TryExecuteTask(task);

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

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

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

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

そのオーバーロードはTask.Factory.StartNew、を受け入れません。ターゲットの場合は、TaskSchedulerからのスケジューラを使用します。つまり、この神話のキューに入れられたタスク内から呼び出すと、もキューに入れられ、その結果、呼び出しはタスクを同期的に実行します。これについてまったく心配している場合(たとえば、ライブラリを実装していて、どこから呼び出されるかわからない場合)、明示的に呼び出しに渡して、(常にに行く)を使用できます。または、createdtotargetを使用します。TaskFactoryTask.FactoryTaskScheduler.CurrentTask.Factory.StartNewRunSynchronouslyTaskSchedulerRunSynchronouslyTaskSchedulerStartNewTaskScheduler.DefaultStartNewTask.RunTaskScheduler.DefaultTaskFactoryTaskScheduler.Default


編集:さて、私は完全に間違っていたようです、そして現在タスクを待っているスレッドはハイジャックされる可能性があります。これが起こっているより簡単な例は次のとおりです。

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

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

于 2012-09-03T10:26:31.243 に答える
4

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

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

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

(副次的な考え:セマフォはデクリメントせずに再入可能ですか?)

于 2012-09-04T13:22:33.587 に答える
0

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

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

于 2012-09-04T07:54:19.683 に答える
0

アーウィンによって提案された解決策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;

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

于 2016-09-01T07:09:58.650 に答える