web-dev-qa-db-ja.com

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

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

  • 呼び出し元が_TaskCompletionSource<T>_でアタッチできないようにする_TaskContinuationOptions.ExecuteSynchronously_を伝える、または
  • 代わりにプールを使用して、_TaskContinuationOptions.ExecuteSynchronously_を無視するように指定する方法で結果(SetResult/TrySetResult)を設定します

具体的には、私が抱えている問題は、着信データが専用のリーダーで処理されていることです。呼び出し元が_TaskContinuationOptions.ExecuteSynchronously_でアタッチできる場合、リーダーを停止させる可能性があります。以前は、any継続が存在するかどうかを検出するハッカーによってこれを回避し、それが完了した場合は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();
    }
}
_
80
Marc Gravell

。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);

編集:

Reflectionを使用する代わりに、式を使用することをお勧めします。これははるかに読みやすく、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();
}
48
Eli Arbel

TPLにはTaskCompletionSource.SetResult継続に対するexplicitAPIコントロールを提供するものはないと思います。 async/awaitシナリオでこの動作を制御するために 初期回答 を維持することにしました。

ContinueWithが呼び出されたのと同じスレッドでtcs.SetResult- triggered継続が行われた場合、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();
    }
}

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

私は呼び出し側を制御しません-特定の継続型バリアントを使用するようにそれらを取得することはできません:できれば、問題はそもそも存在しません

あなたが発信者をコントロールしていないことに気づきませんでした。それにも関わらず、もしあなたがそれを制御しなければ、おそらくTaskCompletionSourceオブジェクトを直接呼び出し元に渡さないでしょう。論理的には、token部分、つまり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);

これは実際にはawaitContinueWithfiddle))およびリフレクションハッキングはありません。

9
noseratio

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

その後、 継続オブジェクトのチェック に似た実装を行いましたが、実際には与えられたコードに対してあまりにも多くのシナリオがあるため、any継続をチェックするだけです。うまく機能しますが、それはTask.Waitは、スレッドプールのルックアップになりました。

最終的に、多くのILを検査した後、唯一の安全で有用なシナリオはSetOnInvokeMresシナリオ(manual-reset-event-slim continuation)です。他にも多くのシナリオがあります。

  • いくつかは安全ではなく、スレッドのハイジャックにつながります
  • 残りは最終的にスレッドプールにつながるため、有用ではありません

そのため、最終的に、非ヌルの継続オブジェクトをチェックすることにしました。 nullの場合は問題ありません(継続なし)。 nullでない場合、SetOnInvokeMresの特殊なケースのチェック-それである場合:fine(呼び出しても安全);それ以外の場合、スプーフィングアボートなどの特別なことをタスクに指示せずに、スレッドプールにTrySetCompleteを実行させます。 Task.WaitSetOnInvokeMresアプローチを使用します。これは、デッドロックにならないように本当に頑張りたい特定のシナリオです。

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>));
3
Marc Gravell

する代わりに

var task = source.Task;

代わりにこれを行います

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

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

3
Ivan Zlatanov

更新ContinueWithではなくawaitを処理するために 個別の回答 を投稿しました(ContinueWithは現在の同期コンテキストを考慮しないため)。

ダム同期コンテキストを使用して、TaskCompletionSourceで_SetResult/SetCancelled/SetException_を呼び出すことによってトリガーされる継続時に非同期を課すことができます。現在の同期コンテキスト(_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_動作を強制します(ターゲット同期コンテキストがない場合はプールスレッドの継続)。

UpdatedContinueWithは現在の同期コンテキストを考慮しないため、これは_task.ContinueWith_では機能しません。ただし、_await task_( fiddle )では機能します。 await task.ConfigureAwait(false)でも機能します。

OTOH、 このアプローチContinueWithに対して機能します。

3
noseratio

リフレクションを使用でき、準備ができている場合は、これを実行する必要があります。

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);
    }        
}
3
Fredou