C# NLog的线程安全性如何?

C# NLog的线程安全性如何?,c#,multithreading,logging,nlog,C#,Multithreading,Logging,Nlog,那么 我已经等了好几天才决定发表这篇文章,因为我不知道该如何陈述这一点,于是又写了一篇详细的文章。然而,我认为在这一点上寻求社区的帮助是相关的 基本上,我尝试使用NLog为数百个线程配置记录器。我认为这将非常简单,但我在几十秒后得到了这个异常: “InvalidOperationException:集合已修改;枚举操作可能无法执行” 这是代码 //Launches threads that initiate loggers class ThreadManager { //(...)

那么

我已经等了好几天才决定发表这篇文章,因为我不知道该如何陈述这一点,于是又写了一篇详细的文章。然而,我认为在这一点上寻求社区的帮助是相关的

基本上,我尝试使用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;
    }
}   
//启动启动记录器的线程
类线程管理器
{
//(...)

对于(int i=0;i我认为您可能没有正确使用
lock
。您正在锁定传递到方法中的对象,因此我认为这意味着不同的线程可能会锁定不同的对象

大多数人只是将此添加到他们的类中:

private static readonly object Lock = new object();
然后锁定它,并保证它始终是同一个对象

我不确定这是否能解决你的问题,但你现在所做的对我来说很奇怪。
其他人可能会不明白。

我不知道NLog,但从上面的片段和API文档中我可以看到(http://nlog-project.org/help/),只有一个静态配置。因此,如果您希望仅在创建记录器时使用此方法将规则添加到配置中(每个规则来自不同的线程),您正在编辑相同的配置对象。据我在NLog文档中看到的,没有办法为每个记录器使用单独的配置,因此您需要所有规则。 添加规则的最佳方法是在启动异步工作程序之前添加规则,但我假设这不是您想要的。 也可以只对所有工作人员使用一个记录器,但我假设您需要将每个工作人员都放在一个单独的文件中。 如果每个线程都在创建自己的记录器并将自己的规则添加到配置中,则必须对其进行锁定。请注意,即使同步代码,在更改规则时,仍有可能有其他代码正在枚举这些规则。如您所示,NLog不会对这些代码位进行锁定。So我假设任何线程安全声明都只针对实际的日志写入方法。 我不确定你现有的锁是做什么的,但我不认为它没有达到你的目的。所以,改变吧

...
lock (LogManager.Configuration.LoggingRules)
config.LoggingRules.Add(Rule);                

LogManager.Configuration = config;
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();
...

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

根据您的代码,您似乎希望为每个线程创建一个记录器,并且希望将该记录器日志记录到一个以传入的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
fileName="${mdc:myid}.log"
fileName="${basedir}/${mdc:myid}.log"
aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log
例如,您可以使用这样的模式基于线程id创建输出文件:

fileName="${threadid}.log"
如果您最终拥有线程101和102,那么您将拥有两个日志文件:101.log和102.log

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

文件名的模式如下所示:

Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log
fileName="${mdc:myid}.log"
fileName="${basedir}/${mdc:myid}.log"
aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.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”的规则指向文件名布局如下的文件目标:

Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log
fileName="${mdc:myid}.log"
fileName="${basedir}/${mdc:myid}.log"
aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log
在记录消息时,NLog将根据文件名布局的值(即,它在日志时应用格式化标记)确定文件名应该是什么。如果文件存在,则将消息写入其中。如果文件尚不存在,则创建文件并将消息记录到其中

如果每个线程都有一个随机id,如“aaaaaaaaaaaa”、“aaaaaaaaaaaab”、“aaaaaaaaaaaaaaaaaac”,那么您应该得到如下日志文件:

Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log
fileName="${mdc:myid}.log"
fileName="${basedir}/${mdc:myid}.log"
aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log
等等

如果您可以这样做,那么您的生活应该会更简单,因为您不必对NLog进行所有编程配置(创建规则和文件目标)。而且您可以让NLog担心创建输出文件名

我不确定这是否会比您以前做的更好。或者,即使这样,您可能真的需要在更大的范围内了解您正在做的事情。测试它应该足够容易,以查看它是否有效(即,您可以根据MappedDiagnosticContext中的值命名输出文件)。如果它能解决这个问题,那么您可以在创建数千个线程的情况下尝试它。