线程安全如何是NLog?

好,

我已经等了好几天才决定发布这个问题,因为我不知道如何说明这一点,重新进入一篇详细的post。 但是,我认为在这一点上要求社区提供帮助是相关的。

基本上,我尝试使用NLog为数百个线程配置记录器。 我认为这将非常简单,但我在几十秒后得到了这个exception:“ InvalidOperationException:Collection被修改;枚举操作可能无法执行

这是代码。

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

所以我完成了我的工作,而不仅仅是在这里发布我的问题,并且有一个家庭质量的时间,我花了一周的时间来挖掘它(幸运男孩!)我下载了最新的稳定版NLOG 2.0并将其包含在我的工作中项目。 我能够追踪它爆炸的确切位置:

在LogFactory.cs中:

  internal void GetTargetsByLevelForLogger(string name, IList rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel) { //lock (rules)//<--Adding this does not fix it foreach (LoggingRule rule in rules)//<-- BLOWS HERE { } } 

在LoggingConfiguration.cs中:

 internal void FlushAllTargets(AsyncContinuation asyncContinuation) { var uniqueTargets = new List(); //lock (LoggingRules)//<--Adding this does not fix it foreach (var rule in this.LoggingRules)//<-- BLOWS HERE { } } 

根据我的问题
因此,根据我的理解,当调用 GetTargetsByLevelForLoggerFlushAllTargets时 ,由于从不同的线程调用config.LoggingRules.Add(Rule), LogManager会混淆。 我试图拧掉foreach并用for循环替换它但是记录器变成了流氓(跳过许多日志文件创建)

最后SOoooo
随处可见的是,NLOG是线程安全的,但我已经通过一些post进一步深入研究并声称这取决于使用情况。 我的情况怎么样? 我必须创建数千个记录器(不是所有记录器同时,但仍然以非常高的速度)。

我找到的解决方法是在SAME MASTER THREAD中创建所有记录器; 这真的很不方便,因为我在应用程序的开头创建了所有的应用程序记录器(类似于记录器池)。 虽然它很好用,但它不是一个可接受的设计。

所以你知道所有的人。 请帮助编码员再次见到他的家人。

我对你的问题没有真正的答案,但我确实有一些观察和一些问题:

根据您的代码,您似乎想要为每个线程创建一个记录器,并且您希望将该记录器日志记录到以某个传入的id值命名的文件中。 因此,id为“abc”的记录器将记录为“x:\ abc.log”,“def”将记录到“x:\ def.log”,依此类推。 我怀疑您可以通过NLog配置而不是以编程方式执行此操作。 我不知道它是否会更好,或者如果NLog会遇到与你相同的问题。

我的第一印象是你做了很多工作:为每个线程创建一个文件目标,为每个线程创建一个新规则,获取一个新的记录器实例等,你可能不需要这样做来完成你想要的东西去完成。

我知道NLog允许动态命名输出文件,至少基于一些NLog LayoutRenderers。 例如,我知道这有效:

 fileName="${level}.log" 

并会给你这样的文件名:

 Trace.log Debug.log Info.log Warn.log Error.log Fatal.log 

因此,例如,您似乎可以使用这样的模式来创建基于线程ID的输出文件:

 fileName="${threadid}.log" 

如果您最终拥有线程101和102,那么您将拥有两个日志文件:101.log和102.log。

在您的情况下,您希望根据自己的ID命名文件。 您可以将id存储在MappedDiagnosticContext(这是一个允许您存储线程本地名称 – 值对的字典)中,然后在您的模式中引用它。

您的文件名模式如下所示:

 fileName="${mdc:myid}.log" 

因此,在您的代码中,您可能会这样做:

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

这样的东西应该允许你的ThreadManager类有一个名为“ThreadManager”的记录器。 每次记录消息时,它都会在Info调用中记录格式化的字符串。 如果记录器配置为记录到文件目标(在配置文件中创建一个将“* .ThreadManager”发送到文件目标的规则,其文件名布局如下所示:

 fileName="${basedir}/${mdc:myid}.log" 

在记录消息时,NLog将根据fileName布局的值确定文件名应该是什么(即它在日志时应用格式化标记)。 如果文件存在,则将消息写入其中。 如果该文件尚不存在,则创建该文件并将消息记录到该文件中。

如果每个线程都有一个随机id,如“aaaaaaaaaaa”,“aaaaaaaaaa”,“aaaaaaaaaa”,那么你应该得到这样的日志文件:

 aaaaaaaaaaaa.log aaaaaaaaaaab.log aaaaaaaaaaac.log 

等等。

如果你可以这样做,那么你的生活应该更简单,因为你没有NLog的所有程序化配置(创建规则和文件目标)。 你可以让NLog担心创建输出文件名。

我不确定这会比你做的更好。 或者,即使它确实如此,你可能真的需要在你的大局中做你正在做的事情。 它应该很容易测试,看它甚至可以工作(即你可以根据MappedDiagnosticContext中的值命名输出文件)。 如果它适用于那个,那么你可以尝试在你创建数千个线程的情况下。

更新:

以下是一些示例代码:

使用这个程序:

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

这个NLog.config文件:

           

我可以为委托的每次执行获取一个日志文件。 日志文件以存储在MDC(MappedDiagnosticContext)中的“id”命名。

因此,当我运行示例程序时,我得到50个日志文件,每个文件中有三行“Enter …”,“Hello …”,“Exit …”。 每个文件名为log__X_.txt ,其中X是捕获的计数器(ii)的值,因此我有log_0 .txt,log_ 1 .txt,log_ 1 .txt等,log_ 49 .txt。 每个日志文件仅包含与一次委托执行有关的日志消息。

这与你想做的相似吗? 我的示例程序使用的是Tasks而不是线程,因为我之前已经编写过它。 我认为这项技术应该能够轻松适应你的工作。

您也可以这样做(使用相同的NLog.config文件为代理的每个执行获取一个新的记录器):

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

我不知道NLog,但从我从上面的部分和API文档(http://nlog-project.org/help/)中可以看到,只有一个静态配置。 因此,如果您只想在创建记录器时(每个来自不同的线程)使用此方法向配置添加规则,那么您正在编辑相同的配置对象。 就我在NLog文档中看到的而言,没有办法为每个记录器使用单独的配置,因此这就是您需要所有规则的原因。

添加规则的最佳方法是在启动异步工作程序之前添加规则,但我会假设这不是您想要的。

也可以为所有工人使用一个记录器。 但我要假设你需要一个单独的文件中的每个工人。

如果每个线程都在创建自己的记录器并将自己的规则添加到配置中,则必须对其进行锁定。 请注意,即使您同步代码,在更改规则时,某些其他代码仍有可能枚举规则。 如图所示,NLog不会锁定这些代码。 所以我假设任何线程安全的声明仅适用于实际的日志写入方法。

我不确定你现有的锁是什么,但我不认为它没有按照你的意图行事。 所以,改变

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

请注意,最好只锁定您“拥有”的对象,即对您的类是私有的对象。 这可以防止某些其他代码中的某些类(不遵循最佳实践)锁定可能会造成死锁的相同代码。 所以我们应该将privateConfigLock定义为您的类的私有。 我们也应该将它设置为静态,以便每个线程都看到相同的对象引用,如下所示:

 public class Logging{ // object used to synchronize the addition of config rules and logger creation private static readonly object privateConfigLock = new object(); ... 

我想你可能没有正确使用lock 。 您正在锁定传递给方法的对象,因此我认为这意味着不同的线程可能会锁定不同的对象。

大多数人只是将它添加到他们的class级:

 private static readonly object Lock = new object(); 

然后锁定它并保证它将始终是同一个对象。

我不确定这会解决你的问题,但你现在所做的事情对我来说很奇怪。 其他人可能会不同意。

通过枚举集合的副本,您可以解决此问题。 我对NLog源做了以下更改,似乎解决了这个问题:

 internal void GetTargetsByLevelForLogger(string name, IList rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel) { foreach (LoggingRule rule in rules.ToList()) { ... 

根据他们的维基上的文档,他们是线程安全的:

因为记录器是线程安全的,所以您只需创建一次记录器并将其存储在静态变量中

根据您得到的错误,您最有可能完成exception的操作:在foreach循环迭代时修改代码中的某个位置。 由于您的列表是通过引用传递的,因此不难想象在您迭代它时,另一个线程将修改规则集合的位置。 你有两个选择:

  1. 预先创建所有规则,然后批量添加(这是我认为你想要的)。
  2. 将您的列表转换为一些只读集合( rules.AsReadOnly() ),但会错过在您生成的线程创建新规则时发生的任何更新。