タスクの同期継続を防ぐにはどうすればよいですか?


83

Task基づいて、リクエストへの保留中の応答にベースのAPIを提供するライブラリ(ソケットネットワーク)コードがありますTaskCompletionSource<T>。ただし、同期継続を防ぐことは不可能であるように思われるという点で、TPLには煩わしさがあります。私はなりたいと行うことができるようにすることのいずれかです:

  • TaskCompletionSource<T>発信者が、、TaskContinuationOptions.ExecuteSynchronouslyまたはで接続することを許可してはならないことを伝えます
  • 代わりにプールを使用して、無視する必要があることを指定する方法で結果(SetResult/ TrySetResult)を設定しTaskContinuationOptions.ExecuteSynchronouslyます

具体的には、私が抱えている問題は、受信データが専用のリーダーによって処理されており、発信者が接続できる場合TaskContinuationOptions.ExecuteSynchronously、リーダーをストールさせる可能性があることです(これはそれらだけに影響を与えるだけではありません)。以前、私はかどうかを検出することをいくつかの牛車でこれを回避働いている任意の継続が存在し、それらがある場合、それはへの完了を押してThreadPool完了が処理されませうとして、発信者が、自分の仕事のキューを飽和している場合は、しかし、これは重大な影響を持っていますタイムリーに。彼らが使用しているTask.Wait()(または同様の)場合、彼らは本質的に彼ら自身をデッドロックさせます。同様に、これが、リーダーがワーカーを使用するのではなく、専用のスレッドを使用している理由です。

そう; TPLチームを悩ませる前に:オプションがありませんか?

キーポイント:

  • 外部の発信者が自分のスレッドを乗っ取れるようにしたくない
  • ThreadPoolプールが飽和状態のときに機能する必要があるため、実装として使用することはできません

以下の例では、出力が生成されます(順序はタイミングによって異なる場合があります)。

Continuation on: Main thread
Press [return]
Continuation on: Thread pool

問題は、ランダムな呼び出し元が「メインスレッド」で継続を取得できたという事実です。実際のコードでは、これはプライマリリーダーを中断します。悪いこと!

コード:

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

static class Program
{
    static void Identify()
    {
        var thread = Thread.CurrentThread;
        string name = thread.IsThreadPoolThread
            ? "Thread pool" : thread.Name;
        if (string.IsNullOrEmpty(name))
            name = "#" + thread.ManagedThreadId;
        Console.WriteLine("Continuation on: " + name);
    }
    static void Main()
    {
        Thread.CurrentThread.Name = "Main thread";
        var source = new TaskCompletionSource<int>();
        var task = source.Task;
        task.ContinueWith(delegate {
            Identify();
        });
        task.ContinueWith(delegate {
            Identify();
        }, TaskContinuationOptions.ExecuteSynchronously);
        source.TrySetResult(123);
        Console.WriteLine("Press [return]");
        Console.ReadLine();
    }
}

2
TaskCompletionSource直接呼び出さないように、独自のAPIでラップしようとしますContinueWith。これはTaskCompletionSource、が、またはTaskそれらからの継承に適していないためです。
デニス

1
@Dennis明確にするために、実際にTask公開されているのはであり、ではありませんTaskCompletionSource。それ(別のAPIを公開する)は技術的にはオプションですが、これだけを行うのはかなり極端なことです...それが正当化されるかどうかは
わかり

2
@MattHは実際にはそうではありません-質問を言い換えるだけです:ThreadPoolこれにforを使用するか(すでに述べました-問題が発生します)、専用の「保留中の継続」スレッドがあり、それら(ExecuteSynchronously指定された継続)がそれを乗っ取ることができます代わりに1つ-これはまったく同じ問題を引き起こします。これは、他のメッセージの継続が停止する可能性があるため、複数の発信者に再び影響を与えます
MarcGravell

3
@Andreyそれ(すべての呼び出し元がexec-syncなしでContinueWithを使用したかのように機能します)はまさに私が達成したいことです。問題は、私のライブラリが誰かにタスクを渡した場合、彼らは非常に望ましくないことをする可能性があることです。彼らはexec-syncを使用して(必然的に)私のリーダーに割り込むことができます。これは非常に危険なので、ライブラリ内に侵入しないようにします
MarcGravell

2
@Andrey理由:a:多くのタスクが最初から継続を取得することはありません(特にバッチ作業を行う場合)-これにより、すべてのタスクに継続が強制さます。b:継続があったタスクでさえ、今でははるかに複雑になっています。オーバーヘッド、およびワーカー操作。これは重要です。
MarcGravell

回答:


50

.NET 4.6の新機能:

.NET 4.6には新しいものが含まれていますTaskCreationOptionsRunContinuationsAsynchronously


Reflectionを使用してプライベートフィールドにアクセスすることをいとわないので...

TCSのタスクにTASK_STATE_THREAD_WAS_ABORTEDフラグを付けることができます。これにより、すべての継続がインライン化されなくなります。

const int TASK_STATE_THREAD_WAS_ABORTED = 134217728;

var stateField = typeof(Task).GetField("m_stateFlags", BindingFlags.NonPublic | BindingFlags.Instance);
stateField.SetValue(task, (int) stateField.GetValue(task) | TASK_STATE_THREAD_WAS_ABORTED);

編集:

リフレクションエミットを使用する代わりに、式を使用することをお勧めします。これははるかに読みやすく、PCL互換であるという利点があります。

var taskParameter = Expression.Parameter(typeof (Task));
const string stateFlagsFieldName = "m_stateFlags";
var setter =
    Expression.Lambda<Action<Task>>(
        Expression.Assign(Expression.Field(taskParameter, stateFlagsFieldName),
            Expression.Or(Expression.Field(taskParameter, stateFlagsFieldName),
                Expression.Constant(TASK_STATE_THREAD_WAS_ABORTED))), taskParameter).Compile();

リフレクションを使用しない場合:

誰かが興味を持っているなら、私はReflectionなしでこれを行う方法を考え出しましたが、それも少し「汚い」ものであり、もちろん無視できないパフォーマンスペナルティがあります。

try
{
    Thread.CurrentThread.Abort();
}
catch (ThreadAbortException)
{
    source.TrySetResult(123);
    Thread.ResetAbort();
}

3
@MarcGravellこれを使用して、TPLチームの疑似サンプルを作成し、コンストラクターオプションなどを介してこれを実行できるようにするための変更要求を行います。
Adam Houldsworth 2014年

1
@Adamええ、このフラグを「原因」ではなく「何をするか」と呼ぶ必要がある場合は、次のようになります。ctorTaskCreationOptions.DoNotInline署名を次のように変更する必要もありませんTaskCompletionSource
。MarcGravell

2
@AdamHouldsworthと心配しないでください、私はすでに彼らに同じメールを送っています; p
MarcGravell

1
あなたの興味のために:ここにあります、ILGeneratorなどを介して最適化されています:github.com/StackExchange/StackExchange.Redis/blob/master/…–
MarcGravell

1
@Noseratioうん、それらをチェックした-ありがとう; それらはすべてOKIMOです。これが純粋な回避策であることに同意しますが、正確に正しい結果が得られます。
MarcGravell

9

TPLには、継続に対する明示的なAPI制御を提供するものはないと思いますTaskCompletionSource.SetResult。私はこの振る舞いを制御するための私の最初の答えasync/awaitシナリオで。

-triggered継続が呼び出されたのと同じスレッドで発生するContinueWith場合、に非同期を課す別のソリューションを次にtcs.SetResult示しますSetResult

public static class TaskExt
{
    static readonly ConcurrentDictionary<Task, Thread> s_tcsTasks =
        new ConcurrentDictionary<Task, Thread>();

    // SetResultAsync
    static public void SetResultAsync<TResult>(
        this TaskCompletionSource<TResult> @this,
        TResult result)
    {
        s_tcsTasks.TryAdd(@this.Task, Thread.CurrentThread);
        try
        {
            @this.SetResult(result);
        }
        finally
        {
            Thread thread;
            s_tcsTasks.TryRemove(@this.Task, out thread);
        }
    }

    // ContinueWithAsync, TODO: more overrides
    static public Task ContinueWithAsync<TResult>(
        this Task<TResult> @this,
        Action<Task<TResult>> action,
        TaskContinuationOptions continuationOptions = TaskContinuationOptions.None)
    {
        return @this.ContinueWith((Func<Task<TResult>, Task>)(t =>
        {
            Thread thread = null;
            s_tcsTasks.TryGetValue(t, out thread);
            if (Thread.CurrentThread == thread)
            {
                // same thread which called SetResultAsync, avoid potential deadlocks

                // using thread pool
                return Task.Run(() => action(t));

                // not using thread pool (TaskCreationOptions.LongRunning creates a normal thread)
                // return Task.Factory.StartNew(() => action(t), TaskCreationOptions.LongRunning);
            }
            else
            {
                // continue on the same thread
                var task = new Task(() => action(t));
                task.RunSynchronously();
                return Task.FromResult(task);
            }
        }), continuationOptions).Unwrap();
    }
}

コメントに対処するために更新されました:

私は発信者を制御していません-特定のcontinue-withバリアントを使用させることはできません:可能であれば、問題はそもそも存在しません

あなたが発信者をコントロールしていないことに気づいていませんでした。それでも、それを制御しない場合は、TaskCompletionSourceオブジェクトを呼び出し元に直接渡していない可能性があります。論理的には、そのトークン部分、つまりを渡すことになりますtcs.Task。その場合、上記に別の拡張メソッドを追加することで、解決策がさらに簡単になる可能性があります。

// ImposeAsync, TODO: more overrides
static public Task<TResult> ImposeAsync<TResult>(this Task<TResult> @this)
{
    return @this.ContinueWith(new Func<Task<TResult>, Task<TResult>>(antecedent =>
    {
        Thread thread = null;
        s_tcsTasks.TryGetValue(antecedent, out thread);
        if (Thread.CurrentThread == thread)
        {
            // continue on a pool thread
            return antecedent.ContinueWith(t => t, 
                TaskContinuationOptions.None).Unwrap();
        }
        else
        {
            return antecedent;
        }
    }), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

使用する:

// library code
var source = new TaskCompletionSource<int>();
var task = source.Task.ImposeAsync();
// ... 

// client code
task.ContinueWith(delegate
{
    Identify();
}, TaskContinuationOptions.ExecuteSynchronously);

// ...
// library code
source.SetResultAsync(123);

これは実際にはとフィドル)の両方awaitで機能しContinueWith、リフレクションハックはありません。


1
私は発信者を制御していません-特定のcontinue-withバリアントを使用させることはできません:可能であれば、問題はそもそも存在しません
MarcGravell

@MarcGravell、発信者を制御できないことに気づいていませんでした。私はそれをどのように扱うかについての最新情報を投稿しました。
noseratio 2014年

図書館の作者のジレンマ; p誰かが、望ましい結果を達成するためのはるかに簡単で直接的な方法を見つけたことに注意してください
MarcGravell

4

する代わりにどうですか

var task = source.Task;

代わりにこれを行います

var task = source.Task.ContinueWith<Int32>( x => x.Result );

したがって、非同期で実行される1つの継続を常に追加しているので、サブスクライバーが同じコンテキストで継続を希望するかどうかは関係ありません。それは一種のカリー化の仕事ですよね?


1
それはコメントで出てきました(アンドレイを参照)。問題はそこにそれがあることで強制的に、彼らは両方のことを何かである、そうでない場合は持っていないときの継続を持っているすべてのタスクをContinueWithしてawait、通常は(など、既に完全をチェックして)を避けるために努力-これは強制であろうから、すべてを上に労働者、それは実際に状況を悪化させるでしょう。これは前向きな考えであり、ありがとうございます。しかし、このシナリオでは役に立ちません。
MarcGravell

3

リフレクションを使用でき、使用する準備ができている場合は、これで十分です。

public static class MakeItAsync
{
    static public void TrySetAsync<T>(this TaskCompletionSource<T> source, T result)
    {
        var continuation = typeof(Task).GetField("m_continuationObject", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
        var continuations = (List<object>)continuation.GetValue(source.Task);

        foreach (object c in continuations)
        {
            var option = c.GetType().GetField("m_options", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
            var options = (TaskContinuationOptions)option.GetValue(c);

            options &= ~TaskContinuationOptions.ExecuteSynchronously;
            option.SetValue(c, options);
        }

        source.TrySetResult(result);
    }        
}

このハックは、フレームワークの次のバージョンでは機能しなくなる可能性があります。
noseratio 2014年

@Noseratio、本当ですが、現在は機能しており、次のバージョンでこれを行うための適切な方法を実装する可能性もあります
Fredou 2014年

しかし、単純にできるのに、なぜこれが必要なのTask.Run(() => tcs.SetResult(result))ですか?
noseratio 2014年

@Noseratio、わかりません、Marcにその質問をしてください:-)、TaskCompletionSourceに接続されているすべてのタスクでフラグTaskContinuationOptions.ExecuteSynchronouslyを削除し、メインスレッドではなくスレッドプールを使用していることを確認します
Fredou

m_continuationObjectハックは、実際には、潜在的に問題のあるタスクを特定するためにすでに使用しているチートです。したがって、これは考慮の余地がありません。興味深い、ありがとう。これは、これまでで最も便利に見えるオプションです。
MarcGravell

3

更新されました、私はではなく対処するために別の答えを投稿ContinueWithしましたawaitContinueWith、現在の同期コンテキストを気にしません)。

あなたは、呼び出しによってトリガ継続時に非同期性を課すためにダムの同期コンテキストを使用することができますSetResult/SetCancelled/SetExceptionTaskCompletionSource。私は現在の同期コンテキストを信じています(await tcs.Task)は、TPLがそのような継続を同期にするか非同期にするかを決定するために使用する基準ます。

以下は私のために働きます:

if (notifyAsync)
{
    tcs.SetResultAsync(null);
}
else
{
    tcs.SetResult(null);
}

SetResultAsync このように実装されます:

public static class TaskExt
{
    static public void SetResultAsync<T>(this TaskCompletionSource<T> tcs, T result)
    {
        FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
    }

    // FakeSynchronizationContext
    class FakeSynchronizationContext : SynchronizationContext
    {
        private static readonly ThreadLocal<FakeSynchronizationContext> s_context =
            new ThreadLocal<FakeSynchronizationContext>(() => new FakeSynchronizationContext());

        private FakeSynchronizationContext() { }

        public static FakeSynchronizationContext Instance { get { return s_context.Value; } }

        public static void Execute(Action action)
        {
            var savedContext = SynchronizationContext.Current;
            SynchronizationContext.SetSynchronizationContext(FakeSynchronizationContext.Instance);
            try
            {
                action();
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(savedContext);
            }
        }

        // SynchronizationContext methods

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }

        public override void OperationStarted()
        {
            throw new NotImplementedException("OperationStarted");
        }

        public override void OperationCompleted()
        {
            throw new NotImplementedException("OperationCompleted");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Post");
        }

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Send");
        }
    }
}

SynchronizationContext.SetSynchronizationContext 追加するオーバーヘッドの点で非常に安価です。実際、WPF実装では非常に類似したアプローチが採用されていますDispatcher.BeginInvoke

TPLは、の時点で目標同期コンテキストを比較awaitの点とtcs.SetResult。同期コンテキストが同じである場合(または両方の場所に同期コンテキストがない場合)、継続は直接、同期的に呼び出されます。それ以外の場合はSynchronizationContext.Post、ターゲット同期コンテキスト、つまり通常のを使用してキューに入れられますawait動作ます。このアプローチが行うことは、常にSynchronizationContext.Post動作(または、ターゲット同期コンテキストがない場合はプールスレッドの継続)をです。

更新されました。現在の同期コンテキストを気にしないためtask.ContinueWith、これContinueWithは機能しません。ただし、await taskfiddle)では機能します。それはまたのために働きますawait task.ConfigureAwait(false)ます。

OTOH、このアプローチContinueWith


魅力的ですが、同期コンテキストを変更すると、呼び出し元のアプリケーションにほぼ確実に影響します。たとえば、ライブラリを使用しているWebまたはWindowsアプリケーションでは、同期コンテキストが1秒間に数百回変更されることはありません。
MarcGravell

@MarcGravell、tcs.SetResult呼び出しの範囲でのみ変更します。継続自体は別のプールスレッドまたは元の同期のいずれかで発生するため、この方法でアトミックでスレッドセーフになります。でキャプチャされawait tcs.Taskたコンテキスト。そして、SynchronizationContext.SetSynchronizationContextそれ自体は非常に安価であり、スレッドスイッチ自体よりもはるかに安価です。
noseratio 2014年

ただし、これは2番目の要件であるを使用しないことを満たさない場合がありますThreadPool。このソリューションでThreadPoolは、同期がなかった場合、TPLは実際にを使用します。のコンテキスト(または基本的なデフォルトのコンテキスト)await tcs.Task。ただし、これは標準のTPL動作です。
noseratio 2014年

うーん... sync-contextはスレッドごとなので、これは実際に実行可能かもしれません-そして私はctxを切り替え続ける必要はありません-ワーカースレッド用に一度設定するだけです。私はそれで遊ぶ必要があります
MarcGravell

1
@Noserationああ、そうです。重要な点がそれらが異なることであるかどうかは明らかではありませんでした。見えます。ありがとう。
MarcGravell

3

アボートシミュレーションアプローチは非常に良さそうに見えましたが、一部のシナリオではTPLハイジャックスレッドつながりました

私はその後に類似していた実装だったの継続オブジェクトをチェックし、ちょうどをチェックする任意のがありましたが、実際には特定のコードがうまく機能するにはシナリオが多すぎる継続をでした。Task.Waitスレッドプールルックアップが発生しました。

最終的に、多くのILを検査した後、安全で有用なシナリオはSetOnInvokeMresシナリオ(手動リセットイベントスリム継続)のみです。他にもたくさんのシナリオがあります。

  • 安全ではないものもあり、スレッドのハイジャックにつながる
  • 残りは最終的にスレッドプールにつながるため、役に立ちません。

したがって、最終的に、null以外の継続オブジェクトをチェックすることにしました。nullの場合、問題ありません(継続なし)。null以外の場合は、特別な場合にチェックしますSetOnInvokeMres-それが次の場合:問題ありません(安全に呼び出すことができます)。それ以外の場合は、TrySetCompleteスプーフィングアボートなどの特別な処理をタスクに指示せずに、スレッドプールにを実行させます。Task.Wait使用してSetOnInvokeMres、我々は試してみたい特定のシナリオでアプローチ、実際にデッドロックすることは難しいことではないが。

Type taskType = typeof(Task);
FieldInfo continuationField = taskType.GetField("m_continuationObject", BindingFlags.Instance | BindingFlags.NonPublic);
Type safeScenario = taskType.GetNestedType("SetOnInvokeMres", BindingFlags.NonPublic);
if (continuationField != null && continuationField.FieldType == typeof(object) && safeScenario != null)
{
    var method = new DynamicMethod("IsSyncSafe", typeof(bool), new[] { typeof(Task) }, typeof(Task), true);
    var il = method.GetILGenerator();
    var hasContinuation = il.DefineLabel();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    Label nonNull = il.DefineLabel(), goodReturn = il.DefineLabel();
    // check if null
    il.Emit(OpCodes.Brtrue_S, nonNull);
    il.MarkLabel(goodReturn);
    il.Emit(OpCodes.Ldc_I4_1);
    il.Emit(OpCodes.Ret);

    // check if is a SetOnInvokeMres - if so, we're OK
    il.MarkLabel(nonNull);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    il.Emit(OpCodes.Isinst, safeScenario);
    il.Emit(OpCodes.Brtrue_S, goodReturn);

    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ret);

    IsSyncSafe = (Func<Task, bool>)method.CreateDelegate(typeof(Func<Task, bool>));
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.