Comment Thread-Safe est NLog?
Eh bien,
J'ai attendu pendant des jours avant de décider de poster ce problème, car je ne savais pas comment l'énoncer, revenant dans un long post détaillé. Cependant, je pense qu'il est pertinent de demander l'aide de la communauté à ce stade.
Fondamentalement, j'ai essayé D'utiliser NLog pour configurer les enregistreurs pour des centaines de threads. Je pensais que ce serait très simple, Mais j'ai eu cette exception après quelques dizaines de secondes : " InvalidOperationException: la Collection a été modifiée; enumeration l'opération ne peut pas exécuter"
Voici le code.
//Launches threads that initiate loggers
class ThreadManager
{
//(...)
for (int i = 0; i<500; i++)
{
myWorker wk = new myWorker();
wk.RunWorkerAsync();
}
internal class myWorker : : BackgroundWorker
{
protected override void OnDoWork(DoWorkEventArgs e)
{
// "Logging" is Not static - Just to eliminate this possibility
// as an error culprit
Logging L = new Logging();
//myRandomID is a random 12 characters sequence
//iLog Method is detailed below
Logger log = L.iLog(myRandomID);
base.OnDoWork(e);
}
}
}
public class Logging
{
//ALL THis METHOD IS VERY BASIC NLOG SETTING - JUST FOR THE RECORD
public Logger iLog(string loggerID)
{
LoggingConfiguration config;
Logger logger;
FileTarget FileTarget;
LoggingRule Rule;
FileTarget = new FileTarget();
FileTarget.DeleteOldFileOnStartup = false;
FileTarget.FileName = "X:\" + loggerID + ".log";
AsyncTargetWrapper asyncWrapper = new AsyncTargetWrapper();
asyncWrapper.QueueLimit = 5000;
asyncWrapper.OverflowAction = AsyncTargetWrapperOverflowAction.Discard;
asyncWrapper.WrappedTarget = FileTarget;
//config = new LoggingConfiguration(); //Tried to Fool NLog by this trick - bad idea as the LogManager need to keep track of all config content (which seems to cause my problem;
config = LogManager.Configuration;
config.AddTarget("File", asyncWrapper);
Rule = new LoggingRule(loggerID, LogLevel.Info, FileTarget);
lock (LogManager.Configuration.LoggingRules)
config.LoggingRules.Add(Rule);
LogManager.Configuration = config;
logger = LogManager.GetLogger(loggerID);
return logger;
}
}
Donc, j'ai fait mon travail plutôt que de simplement poster ma question ici et d'avoir un temps de qualité en famille, j'ai passé le week-end à creuser dessus (garçon chanceux ! ) J'ai téléchargé la dernière version stable de NLog 2.0 et l'ai incluse dans mon projet. J'ai pu tracer l'endroit exact où il a soufflé:
Dans LogFactory.cs:
internal void GetTargetsByLevelForLogger(string name, IList<LoggingRule> rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel)
{
//lock (rules)//<--Adding this does not fix it
foreach (LoggingRule rule in rules)//<-- BLOWS HERE
{
}
}
Dans LoggingConfiguration.cs:
internal void FlushAllTargets(AsyncContinuation asyncContinuation)
{
var uniqueTargets = new List<Target>();
//lock (LoggingRules)//<--Adding this does not fix it
foreach (var rule in this.LoggingRules)//<-- BLOWS HERE
{
}
}
Le problème selon moi
Donc, d'après ma compréhension, ce qui se passe, c'est que le LogManager est mélangé car il y a des appels à config.LoggingRules.Ajouter (règle) à partir de différents threads tandis que GetTargetsByLevelForLogger et FlushAllTargets sont appelés.
J'ai essayé de visser le foreach et de le remplacer par une boucle for mais l'enregistreur est devenu voyou (en ignorant la création de nombreux fichiers journaux)
SOoooo enfin
Il est écrit partout que NLOG est thread-safe, mais Je suis venu à travers quelques messages qui creusent les choses plus loin et prétendent que cela dépend du scénario d'utilisation. Ce sujet de mon cas ?
Je dois créer des milliers d'enregistreurs (pas tous en même temps, mais toujours à un rythme très élevé).
La solution que j'ai trouvée est de créer tous les enregistreurs dans le même THREAD maître; c'est vraiment peu pratique, car je crée tous mes enregistreurs d'applications au début de l'application (sorte de Pool D'enregistreurs). Et bien que cela fonctionne doux, il est tout simplement pas acceptable conception.
Donc vous le savez tous les gens. Aidez un codeur à revoir sa famille.
5 réponses
Je n'ai pas vraiment de réponse à votre problème, mais j'ai quelques observations et quelques questions:
Selon votre code, il semble que vous souhaitiez créer un enregistreur par thread et que vous souhaitiez avoir ce journal dans un fichier nommé pour une valeur d'id transmise. Ainsi, l'enregistreur dont l'id est " abc " se connecterait à "x:\abc.log"," def " se connecterait à "x:\def.journal", et ainsi de suite. Je soupçonne que vous pouvez le faire via la configuration NLog plutôt que par programmation. Je ne sais pas si il serait mieux, ou si NLog aurait le même problème que vous rencontrez.
Ma première impression est que vous faites beaucoup de travail: créer une cible de fichier par thread, créer une nouvelle règle par thread, obtenir une nouvelle instance de logger, etc., que vous n'avez peut-être pas besoin de faire pour accomplir ce que vous voulez accomplir.
Je sais que NLog permet au fichier de sortie d'être nommé dynamiquement, basé sur au moins certains des LayoutRenderers NLog. Par exemple, je sais que ce travaux:
fileName="${level}.log"
Et vous donnera des noms de fichiers comme ceci:
Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log
Ainsi, par exemple, il semble que vous puissiez utiliser un modèle comme celui-ci pour créer des fichiers de sortie basés sur l'id de thread:
fileName="${threadid}.log"
Et si vous finissiez par avoir des threads 101 et 102, alors vous auriez deux fichiers journaux: 101.journal et 102.journal.
Dans votre cas, vous voulez nommer le fichier en fonction de votre propre id. Vous pouvez stocker l'id dans le MappedDiagnosticContext (qui est un dictionnaire qui vous permet de stocker paires nom-valeur thread-local), puis référencez-le dans votre modèle.
Votre modèle pour votre nom de fichier ressemblerait à ceci:
fileName="${mdc:myid}.log"
Donc, dans votre code, vous pouvez faire ceci:
public class ThreadManager
{
//Get one logger per type.
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
protected override void OnDoWork(DoWorkEventArgs e)
{
// Set the desired id into the thread context
NLog.MappedDiagnosticsContext.Set("myid", myRandomID);
logger.Info("Hello from thread {0}, myid {1}", Thread.CurrentThread.ManagedThreadId, myRandomID);
base.OnDoWork(e);
//Clear out the random id when the thread work is finished.
NLog.MappedDiagnosticsContext.Remove("myid");
}
}
Quelque chose comme ça devrait permettre à votre classe ThreadManager d'avoir un seul enregistreur nommé "ThreadManager". Chaque fois qu'il enregistre un message, il enregistre la chaîne formatée dans L'appel Info. Si l'enregistreur est configuré pour se connecter à la cible du fichier (dans le fichier de configuration, créez une règle qui envoie "*.ThreadManager " à une cible de fichier dont la disposition du nom de fichier est quelque chose comme ceci:
fileName="${basedir}/${mdc:myid}.log"
Au moment où un message est enregistré, NLog déterminera quel devrait être le nom de fichier, en fonction de la valeur de la disposition du nom de fichier (c'est-à-dire qu'il applique les jetons de formatage au moment du journal). Si le fichier existe, le message y est écrit. Si le fichier n'existe pas encore, le fichier est créé et le message est enregistré à elle.
Si chaque thread a un id aléatoire comme " aaaaaaaaaaaa", "aaaaaaaaaaab"," aaaaaaaaaaac", alors vous devriez obtenir des fichiers journaux comme ceci:
aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log
Et ainsi de suite.
Si vous pouvez le faire de cette façon, alors votre vie devrait être plus simple car vous n'avez pas à toute cette configuration programmatique de NLog (création de règles et de cibles de fichiers). Et vous pouvez laisser NLog s'inquiéter de la création des noms de fichiers de sortie.
Je ne sais pas pour sûr que cela fonctionnera mieux que ce que vous faisiez. Ou, même si c'est le cas, vous pourriez vraiment besoin de ce que vous êtes faire de votre image plus grande. Il devrait être assez facile de tester pour voir que cela fonctionne même (c'est-à-dire que vous pouvez nommer votre fichier de sortie en fonction d'une valeur dans le MappedDiagnosticContext). Si cela fonctionne pour que, vous pouvez l'essayer pour votre cas où vous créez des milliers de threads.
Mise à jour:
Voici un exemple de code:
Utilisation de ce programme:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using System.Threading;
using System.Threading.Tasks;
namespace NLogMultiFileTest
{
class Program
{
public static Logger logger = LogManager.GetCurrentClassLogger();
static void Main(string[] args)
{
int totalThreads = 50;
TaskCreationOptions tco = TaskCreationOptions.None;
Task task = null;
logger.Info("Enter Main");
Task[] allTasks = new Task[totalThreads];
for (int i = 0; i < totalThreads; i++)
{
int ii = i;
task = Task.Factory.StartNew(() =>
{
MDC.Set("id", "_" + ii.ToString() + "_");
logger.Info("Enter delegate. i = {0}", ii);
logger.Info("Hello! from delegate. i = {0}", ii);
logger.Info("Exit delegate. i = {0}", ii);
MDC.Remove("id");
});
allTasks[i] = task;
}
logger.Info("Wait on tasks");
Task.WaitAll(allTasks);
logger.Info("Tasks finished");
logger.Info("Exit Main");
}
}
}
Et ce NLog.fichier de configuration:
<?xml version="1.0" encoding="utf-8" ?>
<!--
This file needs to be put in the application directory. Make sure to set
'Copy to Output Directory' option in Visual Studio.
-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="file" xsi:type="File" layout="${longdate} | ${processid} | ${threadid} | ${logger} | ${level} | id=${mdc:id} | ${message}" fileName="${basedir}/log_${mdc:item=id}.txt" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="file" />
</rules>
</nlog>
Je suis en mesure d'obtenir un fichier journal pour chaque exécution de la délégué. Le fichier journal est nommé pour le " id " stocké dans le MDC (MappedDiagnosticContext).
Donc, quand j'exécute l'exemple de programme, j'obtiens 50 fichiers journaux, dont chacun a trois lignes "Enter...", "Bonjour...", "Sortie...". Chaque fichier est nommé log__X_.txt
où X est la valeur du compteur capturé (ii), donc j'ai log_0.txt, log_1.txt, log_1.TXT, etc, log_49.txt. Chaque fichier journal contient uniquement les messages de journal relatifs à une exécution délégué.
Est-ce similaire à ce que vous voulez faire? Mon exemple de programme utilise des tâches plutôt que des threads parce que je l'avais déjà écrit il y a quelque temps. Je pense que la technique doit s'adapter à ce que vous faites assez facilement.
Vous pouvez également le faire de cette façon (obtenir un nouvel enregistreur pour chaque exécution du délégué), en utilisant le même NLog.fichier de configuration:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using System.Threading;
using System.Threading.Tasks;
namespace NLogMultiFileTest
{
class Program
{
public static Logger logger = LogManager.GetCurrentClassLogger();
static void Main(string[] args)
{
int totalThreads = 50;
TaskCreationOptions tco = TaskCreationOptions.None;
Task task = null;
logger.Info("Enter Main");
Task[] allTasks = new Task[totalThreads];
for (int i = 0; i < totalThreads; i++)
{
int ii = i;
task = Task.Factory.StartNew(() =>
{
Logger innerLogger = LogManager.GetLogger(ii.ToString());
MDC.Set("id", "_" + ii.ToString() + "_");
innerLogger.Info("Enter delegate. i = {0}", ii);
innerLogger.Info("Hello! from delegate. i = {0}", ii);
innerLogger.Info("Exit delegate. i = {0}", ii);
MDC.Remove("id");
});
allTasks[i] = task;
}
logger.Info("Wait on tasks");
Task.WaitAll(allTasks);
logger.Info("Tasks finished");
logger.Info("Exit Main");
}
}
}
Je ne connais pas NLog, mais d'après ce que je peux voir des morceaux ci-dessus et des documents API(http://nlog-project.org/help/), il n'y a qu'une seule configuration statique. Donc, si vous souhaitez utiliser cette méthode pour ajouter des règles à la configuration uniquement lorsque l'enregistreur est créé (chacun à partir d'un thread différent), vous modifiez le même objet de configuration. Pour autant que je puisse voir dans les documents NLog, il n'y a aucun moyen d'utiliser une configuration séparée pour chaque enregistreur, c'est pourquoi vous avez besoin de toutes les règles.
La meilleure façon pour ajouter des règles à ajouter les règles avant de commencer la async travailleurs, mais je vais supposer que ce n'est pas ce que vous voulez.
, Il serait également possible d'utiliser un seul enregistreur pour tous les travailleurs. Mais je vais supposer que vous avez besoin de chaque travailleur dans un fichier séparé.
Si chaque thread crée son propre enregistreur et ajoute ses propres règles à la configuration, vous devrez mettre un verrou autour de lui. Notez que même avec la synchronisation de votre code, il y a toujours un chance qu'un autre code énumère les règles pendant que vous les modifiez. Comme vous le montrez, NLog ne fait pas de verrouillage autour de ces bits de code. Donc, je suppose que toutes les revendications thread-safe sont pour les méthodes d'écriture de journal réelles seulement.
Je ne suis pas sûr de ce que fait votre verrou existant, mais je ne pense pas qu'il ne fasse pas ce que vous vouliez. Donc, le changement
...
lock (LogManager.Configuration.LoggingRules)
config.LoggingRules.Add(Rule);
LogManager.Configuration = config;
logger = LogManager.GetLogger(loggerID);
return logger;
À
...
lock(privateConfigLock){
LogManager.Configuration.LoggingRules.Add(Rule);
logger = LogManager.GetLogger(loggerID);
}
return logger;
Notez qu'il est considéré comme une bonne pratique de ne verrouiller que les objets que vous 'possédez', c'est-à-dire qui sont privés votre classe. Cela empêche qu'une classe dans un autre code (qui n'adhère pas à la meilleure pratique) se verrouille sur le même code qui peut créer un blocage. Nous devrions donc définir {[3] } comme privé à votre classe. Nous devrions également le rendre statique, de sorte que chaque thread voit la même référence d'objet, comme ceci:
public class Logging{
// object used to synchronize the addition of config rules and logger creation
private static readonly object privateConfigLock = new object();
...
Je pense que vous n'utilisez peut-être pas lock
correctement. Vous verrouillez sur les objets qui sont passés dans votre méthode, donc je pense que cela signifie que différents threads peuvent se verrouiller sur différents objets.
La plupart des gens ajoutent ceci à leur classe:
private static readonly object Lock = new object();
Ensuite, verrouillez-le et soyez assuré que ce sera toujours le même objet.
Je ne suis pas sûr que cela résoudra votre problème, mais ce que vous faites maintenant me semble étrange. D'autres personnes pourraient disaggree.
En énumérant sur une copie de la collection, vous pouvez résoudre ce problème. J'ai fait le changement suivant à la source NLog et il semble résoudre le problème:
internal void GetTargetsByLevelForLogger(string name, IList<LoggingRule> rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel)
{
foreach (LoggingRule rule in rules.ToList<LoggingRule>())
{
...
Selon la documentation sur leur wiki ils sont thread-safe:
Parce que les enregistreurs sont thread-safe, vous pouvez simplement créer l'Enregistreur une fois et le stocker dans une variable statique
Basé sur l'erreur que vous obtenez, vous faites probablement exactement ce que l'exception Sate: modifier la collection quelque part dans le code pendant que la boucle foreach est itérée. Puisque votre liste est passée par référence ce n'est pas difficile d'imaginer où serait un autre thread modification de la collection de règles pendant que vous l'itérez. Vous avez deux options:
- créez toutes vos règles à l'avance, puis ajoutez-les en vrac (c'est ce que je pense que vous voulez).
- Convertissez votre liste dans une collection en lecture seule (
rules.AsReadOnly()
) mais manquez toutes les mises à jour qui se produisent lorsque les threads que vous avez générés créent de nouvelles règles.