web-dev-qa-db-fra.com

Comment puis-je empêcher les continuations synchrones sur une tâche?

J'ai un code de bibliothèque (socket networking) qui fournit une API basée sur Task pour les réponses en attente aux demandes, basée sur TaskCompletionSource<T>. Cependant, il y a une gêne dans le TPL en ce qu'il semble impossible d'empêcher les continuations synchrones. Ce que je voudrais comme pour pouvoir faire est soit:

  • dites à un TaskCompletionSource<T> qui ne devrait pas permettre aux appelants de joindre avec TaskContinuationOptions.ExecuteSynchronously, ou
  • définissez le résultat (SetResult/TrySetResult) d'une manière qui spécifie que TaskContinuationOptions.ExecuteSynchronously doit être ignoré, en utilisant le pool à la place

Plus précisément, le problème que j'ai est que les données entrantes sont traitées par un lecteur dédié, et si un appelant peut joindre avec TaskContinuationOptions.ExecuteSynchronously, Il peut bloquer le lecteur (ce qui affecte plus qu'eux). Auparavant, j'ai contourné ce problème par un piratage qui détecte la présence de toutes continuations, et si elles le sont, cela pousse l'achèvement sur le ThreadPool, mais cela a un impact significatif si l'appelant a saturé leur file d'attente de travail, car l'achèvement ne sera pas traité en temps opportun. S'ils utilisent Task.Wait() (ou similaire), ils se verrouillent alors essentiellement eux-mêmes. De même, c'est pourquoi le lecteur est sur un fil dédié plutôt que d'utiliser des travailleurs.

Donc; avant d'essayer de harceler l'équipe TPL: me manque-t-il une option?

Points clés:

  • Je ne veux pas que les appelants externes puissent pirater mon fil
  • Je ne peux pas utiliser le ThreadPool comme implémentation, car il doit fonctionner lorsque le pool est saturé

L'exemple ci-dessous produit une sortie (l'ordre peut varier en fonction du calendrier):

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

Le problème est le fait qu'un appelant aléatoire a réussi à obtenir une continuation sur "Thread principal". Dans le vrai code, cela interromprait le lecteur principal; mauvaises choses!

Code:

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

Nouveau dans .NET 4.6:

.NET 4.6 contient un nouveau TaskCreationOptions: RunContinuationsAsynchronously.


Puisque vous êtes prêt à utiliser Reflection pour accéder à des champs privés ...

Vous pouvez marquer la tâche du TCS avec le TASK_STATE_THREAD_WAS_ABORTED flag, ce qui entraînerait le non-alignement de toutes les suites.

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

Modifier:

Au lieu d'utiliser Reflection emit, je vous suggère d'utiliser des expressions. Ceci est beaucoup plus lisible et a l'avantage d'être compatible 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();

Sans utiliser Reflection:

Si quelqu'un est intéressé, j'ai trouvé un moyen de le faire sans réflexion, mais c'est aussi un peu "sale", et bien sûr, il porte une pénalité de perf non négligeable:

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

Je ne pense pas qu'il y ait quelque chose dans TPL qui fournisse explicite Contrôle API sur TaskCompletionSource.SetResult suites. J'ai décidé de garder mon réponse initiale pour contrôler ce comportement pendant async/await scénarios.

Voici une autre solution qui impose asynchrone à ContinueWith, si le tcs.SetResult- la poursuite déclenchée a lieu sur le même thread que SetResult a été appelé:

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

Mis à jour pour répondre au commentaire:

Je ne contrôle pas l'appelant - je ne peux pas lui faire utiliser une variante continue spécifique: si je le pouvais, le problème n'existerait pas en premier lieu

Je ne savais pas que vous ne contrôliez pas l'appelant. Néanmoins, si vous ne le contrôlez pas, vous ne passez probablement pas non plus l'objet TaskCompletionSource directement à l'appelant. Logiquement, vous passez la partie token, c'est-à-dire tcs.Task. Dans ce cas, la solution pourrait être encore plus simple, en ajoutant une autre méthode d'extension à ce qui précède:

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

Utilisation:

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

En fait cela fonctionne à la fois pour await et ContinueWith (violon) et est exempt de hacks de réflexion.

9
noseratio

L'approche simuler l'abandon avait l'air vraiment bien, mais a conduit aux threads de détournement de TPL dans certains scénarios .

J'ai ensuite eu une implémentation similaire à vérification de l'objet de continuation , mais juste à la recherche de any continuation car il y a en fait trop de scénarios pour que le code donné fonctionne correctement , mais cela signifiait que même des choses comme Task.Wait a entraîné une recherche de pool de threads.

En fin de compte, après avoir inspecté beaucoup et beaucoup d'IL, le seul scénario sûr et utile est le scénario SetOnInvokeMres (suite manuelle-reset-event-slim). Il existe de nombreux autres scénarios:

  • certains ne sont pas sûrs et conduisent à un détournement de fil
  • le reste n'est pas utile, car ils conduisent finalement au pool de threads

Donc à la fin, j'ai choisi de vérifier un objet de continuation non nul; s'il est nul, très bien (pas de suite); si elle n'est pas nulle, vérifier le cas spécial pour SetOnInvokeMres - si c'est le cas: fine (sûr à invoquer); sinon, laissez le pool de threads effectuer le TrySetComplete, sans demander à la tâche de faire quelque chose de spécial comme l'annulation de l'usurpation d'identité. Task.Wait utilise l'approche SetOnInvokeMres, qui est le scénario spécifique que nous voulons essayer vraiment difficile de ne pas bloquer.

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

Et au lieu de faire

var task = source.Task;

tu fais ça à la place

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

Ainsi, vous ajoutez toujours une continuation qui sera exécutée de manière asynchrone et ensuite peu importe si les abonnés souhaitent une continuation dans le même contexte. C'est en quelque sorte de ralentir la tâche, n'est-ce pas?

3
Ivan Zlatanov

Mis à jour , j'ai posté un réponse séparée pour traiter ContinueWith par opposition à await (parce que ContinueWith ne se soucie pas du contexte de synchronisation actuel).

Vous pouvez utiliser un contexte de synchronisation stupide pour imposer l'asynchronie lors de la poursuite déclenchée en appelant SetResult/SetCancelled/SetException Sur TaskCompletionSource. Je crois que le contexte de synchronisation actuel (au point de await tcs.Task) Est le critère utilisé par TPL pour décider de rendre une telle continuation synchrone ou asynchrone.

Ce qui suit fonctionne pour moi:

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

SetResultAsync est implémenté comme ceci:

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.SetSynchronizationContextest très bon marché en termes de frais généraux qu'il ajoute. En fait, une approche très similaire est adoptée par le implémentation de WPF Dispatcher.BeginInvoke .

TPL compare le contexte de synchronisation cible au point de await à celui du point de tcs.SetResult. Si le contexte de synchronisation est le même (ou s'il n'y a pas de contexte de synchronisation aux deux endroits), la suite est appelée directement, de manière synchrone. Sinon, il est mis en file d'attente à l'aide de SynchronizationContext.Post Sur le contexte de synchronisation cible, c'est-à-dire le comportement normal de await. Cette approche impose toujours le comportement SynchronizationContext.Post (Ou une continuation de thread de pool s'il n'y a pas de contexte de synchronisation cible).

Mis à jour , cela ne fonctionnera pas pour task.ContinueWith, Car ContinueWith ne se soucie pas du contexte de synchronisation actuel. Cela fonctionne cependant pour await task ( violon ). Il fonctionne également pour await task.ConfigureAwait(false).

OTOH, cette approche fonctionne pour ContinueWith.

3
noseratio

si vous pouvez et êtes prêt à utiliser la réflexion, cela devrait le faire;

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