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电话(规则),而GetTargetsByLevelForLogger和FlushAllTargets被调用.我试图拧掉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)
这是一个较旧的问题,但作为 NLog 的当前所有者,我有以下见解:
LoggingConfiguration.AddRule
+ 时ReconfigExistingLoggers
)您应该避免在运行时更改现有项目的值。相反,应该使用情境渲染器(${event-properties}
,${GDC}
,${MDLC}
等)
归档时间: |
|
查看次数: |
17775 次 |
最近记录: |