线程安全如何是NLog?

Meh*_*ANI 27 c# logging multithreading nlog

好,

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

基本上,我尝试使用NLog为数百个线程配置记录器.我认为这将非常简单,但我在几十秒后得到了这个异常:" 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;
    }
}   
Run Code Online (Sandbox Code Playgroud)

所以我完成了我的工作,而不仅仅是在这里发布我的问题并且有一个家庭质量的时间,我花了一周的时间来挖掘它(幸运男孩!)我下载了最新的稳定版本的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
            {
            }
     }
Run Code Online (Sandbox Code Playgroud)

在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
        {
        }
     }
Run Code Online (Sandbox Code Playgroud)

根据我的问题
所以,根据我的理解,什么情况是,日志管理得到混合,因为有来自不同的线程config.LoggingRules.Add电话(规则),而GetTargetsByLevelForLoggerFlushAllTargets被调用.我试图拧掉foreach并用for循环替换它但是记录器变成了流氓(跳过许多日志文件创建)

SOoooo FINALLY
到处写的是NLOG是线程安全的,但我已经通过一些帖子进一步深入挖掘并声称这取决于使用场景.我的情况怎么样?我必须创建数千个记录器(不是所有记录器同时存在,但仍然处于非常高的速度).

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

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

wag*_*ghe 34

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

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

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

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

fileName="${level}.log"
Run Code Online (Sandbox Code Playgroud)

并会给你这样的文件名:

Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log
Run Code Online (Sandbox Code Playgroud)

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

fileName="${threadid}.log"
Run Code Online (Sandbox Code Playgroud)

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

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

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

fileName="${mdc:myid}.log"
Run Code Online (Sandbox Code Playgroud)

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

         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");
           }
         }
Run Code Online (Sandbox Code Playgroud)

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

fileName="${basedir}/${mdc:myid}.log"
Run Code Online (Sandbox Code Playgroud)

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

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

aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log
Run Code Online (Sandbox Code Playgroud)

等等.

如果你可以这样做,那么你的生活应该更简单,因为你没有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");
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这个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>
Run Code Online (Sandbox Code Playgroud)

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


小智 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;
Run Code Online (Sandbox Code Playgroud)

...
lock(privateConfigLock){
    LogManager.Configuration.LoggingRules.Add(Rule);                

    logger = LogManager.GetLogger(loggerID);
}
return logger;
Run Code Online (Sandbox Code Playgroud)

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

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


Jul*_*ian 7

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

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

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