草庐IT

c# - NLog 的线程安全性如何?

coder 2023-07-11 原文

好,

我已经等了好几天才决定发布这个问题,因为我不知道如何说明这一点,所以我写了一篇很长的详细帖子。但是,我认为此时寻求社区的帮助是有意义的。

基本上,我尝试使用 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
        {
        }
     }

根据我的问题
所以,根据我的理解,发生的情况是 LogManager 被混淆了,因为有 从不同线程调用 config.LoggingRules.Add(Rule) 同时调用 GetTargetsByLevelForLogger 和 FlushAllTargets。
我试图搞砸 foreach 并用 for 循环替换它,但记录器变成了流氓(跳过了许多日志文件的创建)

SOoooo 最后
到处都写着 NLOG 是线程安全的,但我已经阅读了一些帖子,这些帖子进一步挖掘并声称这取决于使用场景。我的情况呢?
我必须创建数千个记录器(不是同时创建所有记录器,但速度仍然非常快)。

我发现的解决方法是在同一主线程中创建所有记录器;这真的很不方便,因为我在应用程序开始时创建了我所有的应用程序记录器(有点像记录器池)。
虽然它工作得很好,但它不是一个可以接受的设计。

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

最佳答案

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

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

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

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

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 布局的值确定文件名应该是什么(即它在记录时应用格式标记)。如果文件存在,则将消息写入其中。如果该文件尚不存在,则会创建该文件并将消息记录到该文件中。

如果每个线程都有一个像“aaaaaaaaaaaa”、“aaaaaaaaaaab”、“aaaaaaaaaaac”这样的随机ID,那么你应该得到这样的日志文件:
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>

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

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

这与您想要做的相似吗?我的示例程序使用任务而不是线程,因为我前段时间已经编写了它。我认为该技术应该足够轻松地适应您正在做的事情。

您也可以使用相同的 NLog.config 文件这样做(为委托(delegate)的每次执行获取一个新的记录器):
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");
    }
  }
}

关于c# - NLog 的线程安全性如何?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5706119/

有关c# - NLog 的线程安全性如何?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐