线程安全如何是NLog?

时间:2011-04-18 16:46:12

标签: c# multithreading logging nlog

好吧,

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

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

这是代码。

//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<LoggingRule> 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<Target>();
        //lock (LoggingRules)//<--Adding this does not fix it
        foreach (var rule in this.LoggingRules)//<-- BLOWS HERE
        {
        }
     }

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

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

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

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

6 个答案:

答案 0 :(得分:34)

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

根据您的代码,您似乎想要为每个线程创建一个记录器,并且您希望将该记录器记录到以某个传入的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文件:

<?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>

我可以为委托的每次执行获取一个日志文件。日志文件以存储在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");
    }
  }
}

答案 1 :(得分:7)

我不知道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();
...

答案 2 :(得分:1)

这是一个比较老的问题,但是作为NLog的当前所有者,我有以下见解:

  • 创建记录器是线程安全的
  • 写日志消息是线程安全的
  • 上下文类和渲染器的使用(GDC,MDC等)是线程安全的
  • 在运行时添加新目标和规则是线程安全的(使用LoggingConfiguration.AddRule + ReconfigExistingLoggers时)
  • 执行LoggingConfiguration的重新加载将导致活动记录器的LogEvent被丢弃,直到重新加载完成。
  • 在运行时更改现有规则和目标的值不是线程安全的!

您应避免在运行时更改现有项目的值。相反,应该使用context renderers${event-properties}${GDC}${MDLC}等)

答案 3 :(得分:0)

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

大多数人只是将其添加到他们的班级中:

private static readonly object Lock = new object();

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

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

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

根据their wiki的文档,它们是线程安全的:

  

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

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

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