Question

Avant d'utiliser la bibliothèque parallèle de tâches, je l'ai souvent utilisé CorrelationManager.ActivityId de garder une trace de traçage / rapports d'erreurs avec plusieurs threads.

ActivityId est stocké dans Thread Local Storage, de sorte que chaque get est sa propre copie de fil. L'idée est que lorsque vous lancez un fil (activité), vous attribuez un nouveau ActivityId. Le ActivityId sera écrit dans les journaux toute autre information de trace, permettant d'isoler les informations de trace pour une seule « activité ». Ceci est vraiment utile avec WCF comme ActivityId peut être reporté sur le composant de service.

Voici un exemple de ce dont je parle:

static void Main(string[] args)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
    {
        DoWork();
    }));
}

static void DoWork()
{
    try
    {
        Trace.CorrelationManager.ActivityId = Guid.NewGuid();
        //The functions below contain tracing which logs the ActivityID.
        CallFunction1();
        CallFunction2();
        CallFunction3();
    }
    catch (Exception ex)
    {
        Trace.Write(Trace.CorrelationManager.ActivityId + " " + ex.ToString());
    }
}

Maintenant, avec le TPL, je crois comprendre que plusieurs tâches partagent threads. Est-ce que cela signifie que ActivityId est enclin à être réinitialisée mi-tâche (par une autre tâche)? Y at-il un nouveau mécanisme pour traiter le suivi des activités?

Était-ce utile?

La solution

J'ai couru quelques expériences et il se trouve l'hypothèse dans ma question est incorrecte -. Tâches multiples créées avec le TPL ne fonctionne pas sur le même fil en même temps

est sûr de thread local storage utiliser avec TPL dans .NET 4.0, car un fil ne peut être utilisé par une seule tâche à la fois.

L'hypothèse que les tâches peuvent partager threads simultanément a été basé sur une interview que j'ai entendu parler de c # 5.0 DotNetRocks (désolé, je ne me souviens pas qui était le montrer) - donc ma question peut (ou non) deviennent bientôt pertinentes

.

Mon expérience commence un certain nombre de tâches et dossiers combien de tâches ont couru, combien de temps ils ont pris, et combien de fils ont été consommés. Le code est ci-dessous si quelqu'un veut le répéter.

class Program
{
    static void Main(string[] args)
    {
        int totalThreads = 100;
        TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;
        Task task = null;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        Task[] allTasks = new Task[totalThreads];
        for (int i = 0; i < totalThreads; i++)
        {
            task = Task.Factory.StartNew(() =>
           {
               DoLongRunningWork();
           }, taskCreationOpt);

            allTasks[i] = task;
        }

        Task.WaitAll(allTasks);
        stopwatch.Stop();

        Console.WriteLine(String.Format("Completed {0} tasks in {1} milliseconds", totalThreads, stopwatch.ElapsedMilliseconds));
        Console.WriteLine(String.Format("Used {0} threads", threadIds.Count));
        Console.ReadKey();
    }


    private static List<int> threadIds = new List<int>();
    private static object locker = new object();
    private static void DoLongRunningWork()
    {
        lock (locker)
        {
            //Keep a record of the managed thread used.
            if (!threadIds.Contains(Thread.CurrentThread.ManagedThreadId))
                threadIds.Add(Thread.CurrentThread.ManagedThreadId);
        }
        Guid g1 = Guid.NewGuid();
        Trace.CorrelationManager.ActivityId = g1;
        Thread.Sleep(3000);
        Guid g2 = Trace.CorrelationManager.ActivityId;
        Debug.Assert(g1.Equals(g2));
    }
}

La sortie (bien sûr cela dépendra de la machine) était:

Completed 100 tasks in 23097 milliseconds
Used 23 threads

Modification taskCreationOpt à TaskCreationOptions.LongRunning a donné des résultats différents:

Completed 100 tasks in 3458 milliseconds 
Used 100 threads

Autres conseils

S'il vous plaît pardonnez mon message cela comme une réponse car il est pas vraiment répondre à votre question, cependant, il est lié à votre question car il traite de comportement CorrelationManager et threads / tâches / etc. J'ai cherché à utiliser LogicalOperationStack du CorrelationManager (et les méthodes StartLogicalOperation/StopLogicalOperation) pour fournir un contexte supplémentaire dans les scénarios multithreading.

Je pris votre exemple et modifié légèrement pour ajouter la possibilité d'effectuer des travaux en parallèle en utilisant Parallel.For. J'utilise aussi StartLogicalOperation/StopLogicalOperation sur le support (interne) DoLongRunningWork. Conceptuellement, DoLongRunningWork fait quelque chose comme ça à chaque fois qu'il est exécuté:

DoLongRunningWork
  StartLogicalOperation
  Thread.Sleep(3000)
  StopLogicalOperation

J'ai trouvé que si j'ajoute ces opérations logiques à votre code (plus ou moins est), tous les operatins logiques restent synchronisés (toujours le nombre prévu d'opérations sur pile et les valeurs des opérations sur la pile sont toujours comme prévu).

Dans certains de mes propres tests, j'ai trouvé que ce ne fut pas toujours le cas. La pile d'opération logique était de se « corrompu ». La meilleure explication que je pourrais trouver est que la « fusion » de retour des informations CallContext dans le contexte du thread « parent » lorsque les sorties de fil « enfant » a été à l'origine des informations de contexte de thread enfant « vieux » (opération logique) d'être " hérité » par un autre thread enfant frères et soeurs.

Le problème peut également être lié au fait que Parallel.For utilise apparemment le thread principal (au moins dans le code exemple, comme il est écrit) comme l'un des « threads de travail » (ou ce qu'ils devraient être appelés en parallèle domaine). Chaque fois que DoLongRunningWork est exécuté, une nouvelle opération logique est lancée (au début) et arrêté (à la fin) (qui, poussé sur le dos LogicalOperationStack et sauté hors de celui-ci). Si le thread principal a déjà une opération logique en vigueur et si DoLongRunningWork exécute sur le Main FILET, une nouvelle opération logique est lancée si LogicalOperationStack a maintenant deux opérations du thread principal. Toutes les exécutions ultérieures de DoLongRunningWork (aussi longtemps que cette « itération » de DoLongRunningWork est exécutée sur le thread principal) sera (apparemment) Hériter LogicalOperationStack (qui a maintenant deux opérations sur elle, plutôt que l'opération on prévu) du thread principal.

Il m'a fallu beaucoup de temps pour comprendre pourquoi le comportement du LogicalOperationStack était différent dans mon exemple que dans ma version modifiée de votre exemple. Enfin j'ai vu que dans mon code, je l'avais placé entre crochets tout le programme dans une opération logique, alors que dans ma version modifiée de votre programme de test, je ne l'ai pas. L'implication est que dans mon programme de test, chaque fois que mon a été réalisé « travail » (analogue à DoLongRunningWork), il y avait déjà une opération logique en vigueur. Dans ma version modifiée de votre programme de test, je ne l'avais pas placé entre crochets l'ensemble du programme dans une opération logique.

Alors, quand je modifié votre programme de test sur le support du programme entier dans une opération logique et si je me sers Parallel.For, je suis tombé sur exactement le même problème.

En utilisant le modèle conceptuel ci-dessus, ce sera exécuté avec succès:

Parallel.For
  DoLongRunningWork
    StartLogicalOperation
    Sleep(3000)
    StopLogicalOperation

Bien que cela finira par assert en raison d'une apparence de synchronisation LogicalOperationStack:

StartLogicalOperation
Parallel.For
  DoLongRunningWork
    StartLogicalOperation
    Sleep(3000)
    StopLogicalOperation
StopLogicalOperation

Voici mon exemple de programme. Il est semblable à la vôtre en ce qu'elle a une méthode DoLongRunningWork qui manipule la ActivityId ainsi que la LogicalOperationStack. J'ai aussi deux saveurs de coups de pied DoLongRunningWork. Une saveur utilise les tâches on utilise Parallel.For. Chaque saveur peut également être exécuté de telle sorte que toute l'opération parallélisée est enfermé dans une opération logique ou non. Donc, il y a un total de 4 façons d'exécuter l'opération parallèle. Pour essayer chacun, il suffit de décommenter la méthode souhaitée « Utiliser ... », recompilation et courir. UseTasks, UseTasks(true) et UseParallelFor devraient tous courir à completisur. UseParallelFor(true) affirmera à un moment donné parce que le LogicalOperationStack n'a pas le nombre prévu d'entrées.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace CorrelationManagerParallelTest
{
  class Program 
  {     
    static void Main(string[] args)     
    { 
      //UseParallelFor(true) will assert because LogicalOperationStack will not have expected
      //number of entries, all others will run to completion.

      UseTasks(); //Equivalent to original test program with only the parallelized
                      //operation bracketed in logical operation.
      ////UseTasks(true); //Bracket entire UseTasks method in logical operation
      ////UseParallelFor();  //Equivalent to original test program, but use Parallel.For
                             //rather than Tasks.  Bracket only the parallelized
                             //operation in logical operation.
      ////UseParallelFor(true); //Bracket entire UseParallelFor method in logical operation
    }       

    private static List<int> threadIds = new List<int>();     
    private static object locker = new object();     

    private static int mainThreadId = Thread.CurrentThread.ManagedThreadId;

    private static int mainThreadUsedInDelegate = 0;

    // baseCount is the expected number of entries in the LogicalOperationStack
    // at the time that DoLongRunningWork starts.  If the entire operation is bracketed
    // externally by Start/StopLogicalOperation, then baseCount will be 1.  Otherwise,
    // it will be 0.
    private static void DoLongRunningWork(int baseCount)     
    {
      lock (locker)
      {
        //Keep a record of the managed thread used.             
        if (!threadIds.Contains(Thread.CurrentThread.ManagedThreadId))
          threadIds.Add(Thread.CurrentThread.ManagedThreadId);

        if (Thread.CurrentThread.ManagedThreadId == mainThreadId)
        {
          mainThreadUsedInDelegate++;
        }
      }         

      Guid lo1 = Guid.NewGuid();
      Trace.CorrelationManager.StartLogicalOperation(lo1);

      Guid g1 = Guid.NewGuid();         
      Trace.CorrelationManager.ActivityId = g1;

      Thread.Sleep(3000);         

      Guid g2 = Trace.CorrelationManager.ActivityId;
      Debug.Assert(g1.Equals(g2));

      //This assert, LogicalOperation.Count, will eventually fail if there is a logical operation
      //in effect when the Parallel.For operation was started.
      Debug.Assert(Trace.CorrelationManager.LogicalOperationStack.Count == baseCount + 1, string.Format("MainThread = {0}, Thread = {1}, Count = {2}, ExpectedCount = {3}", mainThreadId, Thread.CurrentThread.ManagedThreadId, Trace.CorrelationManager.LogicalOperationStack.Count, baseCount + 1));
      Debug.Assert(Trace.CorrelationManager.LogicalOperationStack.Peek().Equals(lo1), string.Format("MainThread = {0}, Thread = {1}, Count = {2}, ExpectedCount = {3}", mainThreadId, Thread.CurrentThread.ManagedThreadId, Trace.CorrelationManager.LogicalOperationStack.Peek(), lo1));

      Trace.CorrelationManager.StopLogicalOperation();
    } 

    private static void UseTasks(bool encloseInLogicalOperation = false)
    {
      int totalThreads = 100;
      TaskCreationOptions taskCreationOpt = TaskCreationOptions.None;
      Task task = null;
      Stopwatch stopwatch = new Stopwatch();
      stopwatch.Start();

      if (encloseInLogicalOperation)
      {
        Trace.CorrelationManager.StartLogicalOperation();
      }

      Task[] allTasks = new Task[totalThreads];
      for (int i = 0; i < totalThreads; i++)
      {
        task = Task.Factory.StartNew(() =>
        {
          DoLongRunningWork(encloseInLogicalOperation ? 1 : 0);
        }, taskCreationOpt);
        allTasks[i] = task;
      }
      Task.WaitAll(allTasks);

      if (encloseInLogicalOperation)
      {
        Trace.CorrelationManager.StopLogicalOperation();
      }

      stopwatch.Stop();
      Console.WriteLine(String.Format("Completed {0} tasks in {1} milliseconds", totalThreads, stopwatch.ElapsedMilliseconds));
      Console.WriteLine(String.Format("Used {0} threads", threadIds.Count));
      Console.WriteLine(String.Format("Main thread used in delegate {0} times", mainThreadUsedInDelegate));

      Console.ReadKey();
    }

    private static void UseParallelFor(bool encloseInLogicalOperation = false)
    {
      int totalThreads = 100;
      Stopwatch stopwatch = new Stopwatch();
      stopwatch.Start();

      if (encloseInLogicalOperation)
      {
        Trace.CorrelationManager.StartLogicalOperation();
      }

      Parallel.For(0, totalThreads, i =>
      {
        DoLongRunningWork(encloseInLogicalOperation ? 1 : 0);
      });

      if (encloseInLogicalOperation)
      {
        Trace.CorrelationManager.StopLogicalOperation();
      }

      stopwatch.Stop();
      Console.WriteLine(String.Format("Completed {0} tasks in {1} milliseconds", totalThreads, stopwatch.ElapsedMilliseconds));
      Console.WriteLine(String.Format("Used {0} threads", threadIds.Count));
      Console.WriteLine(String.Format("Main thread used in delegate {0} times", mainThreadUsedInDelegate));

      Console.ReadKey();
    }

  } 
}

Cette question de si LogicalOperationStack peut être utilisé avec Parallel.For (et / ou un autre thread / constructions Tâche) ou comment il peut être utilisé mérite probablement sa propre question. Peut-être que je vais poser une question. En attendant, je me demande si vous avez des idées sur ce (ou, je me demande si vous aviez envisagé d'utiliser LogicalOperationStack depuis ActivityId semble être sans danger).

[EDIT]

Voir ma réponse à ce question pour plus d'informations sur l'utilisation LogicalOperationStack et / ou CallContext.LogicalSetData avec certains des divers discussion / ThreadPool / tâche / parallèle contstructs.

Voir aussi ma question ici sur le SO à propos LogicalOperationStack et extensions parallèles: Est CorrelationManager.LogicalOperationStack compatible avec Parallel.For, tâches, fils, etc

Enfin, vous pouvez aussi consulter ma question ici sur le forum Extensions parallèle de Microsoft: http: // sociale. msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9

Dans mes tests, il ressemble Trace.CorrelationManager.LogicalOperationStack peut être endommagé lors de l'utilisation Parallel.For ou Parallel.Invoke si vous commencez une opération logique dans le thread principal puis démarrer / arrêter les opérations logiques dans le délégué. Dans mes tests (voir l'un des deux liens ci-dessus), le LogicalOperationStack doit toujours avoir exactement 2 entrées lorsque DoLongRunningWork exécute (si je commence une opération logique dans le thread principal avant de coups de pied DoLongRunningWork en utilisant diverses techniques). Ainsi, par « corrompu » Je veux dire que le LogicalOperationStack finira par avoir beaucoup plus de 2 entrées.

D'après ce que je peux dire, cela est probablement dû Parallel.For et Parallel.Invoke utilisent le thread principal comme l'un des fils « travailleur » pour effectuer l'action de DoLongRunningWork.

En utilisant une pile stockée dans CallContext.LogicalSetData pour imiter le comportement du LogicalOperationStack (similaire aux LogicalThreadContext.Stacks de log4net qui est stocké par CallContext.SetData) donne encore plus mauvais résultats. Si je me sers d'une telle pile pour maintenir le contexte, il est corrompu (ie ne dispose pas du nombre d'inscriptions prévu) dans presque tous les scénarios où j'ai une « opération logique » dans le thread principal et une opération logique à chaque itération / exécution du délégué DoLongRunningWork.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top