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
{
}
}
我认为的问题
因此,根据我的理解,发生的情况是 LogManager 会混淆,因为从不同线程调用 config.LoggingRules.Add(Rule),而 GetTargetsByLevelForLogger 和 < em>FlushAllTargets 正在被调用。 我尝试拧紧 foreach 并将其替换为 for 循环,但记录器变得流氓(跳过许多日志文件创建)
SOoooo FINALLY
到处都写着 NLOG 是线程安全的,但我浏览了一些帖子,进一步深入研究并声称这取决于使用场景。我的情况呢? 我必须创建数千个记录器(不是同时创建,但仍然以非常高的速度)。
我发现的解决方法是在同一个主线程中创建所有记录器;这确实很不方便,因为我在应用程序启动时创建了所有应用程序记录器(类似于记录器池)。 尽管它工作起来很甜蜜,但这并不是一个可以接受的设计。
所以你们都知道了。 请帮助一名程序员再次见到他的家人。
Well,
I have waited for days before deciding to post this issue, as I was not sure how to state this, resutling into a long detailed post. However, I think it is relevant to ask for the community's help at this point.
Basically, I tried to use NLog to configure loggers for hundreds of threads. I thought this would be very straightforward, But I got this exception after few tens of seconds :
"InvalidOperationException : Collection was modified; enumeration operation may not execute"
Here is the code.
//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;
}
}
So I did my job an rather than just posting my question here and having a family-quality-time, I spent the week-end digging on that (Lucky Boy ! )
I downloaded the latest stable release of NLOG 2.0 and included it in my project. I was able to trace the exact place where it blowed :
in 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
{
}
}
in 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
{
}
}
The problem according to me
So, based on my understanding, what happens is that the LogManager gets mixed up as there are calls to config.LoggingRules.Add(Rule) from different threads while GetTargetsByLevelForLogger and FlushAllTargets are being called.
I tried to screw the foreach and replace it by a for loop but the logger turned rogue (skipping many log files creation)
SOoooo FINALLY
It is written everywhere that NLOG is thread-safe, but I've come through a few posts that dig things further and claim that this depends on the utilization scenario. What about my case ?
I have to create thousands of loggers (not all at the same time, but still at a very high pace).
The workaround I have found is to create all loggers in the SAME MASTER THREAD; this is REALLY unconvenient, as I create all my app loggers at the start of the app (sort of a Logger Pool).
And though it works sweet, it is just NOT an acceptable design.
So you know it all Folks.
Please help a coder see his family again.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
我并没有真正回答您的问题,但我确实有一些观察和一些问题:
根据您的代码,您似乎想为每个线程创建一个记录器,并且希望将该记录器记录到名为的文件中对于一些传入的 id 值。因此,id 为“abc”的记录器将记录到“x:\abc.log”,“def”将记录到“x:\def.log”,依此类推。我怀疑您可以通过 NLog 配置而不是通过编程来完成此操作。我不知道它是否会更好,或者 NLog 是否会遇到与您相同的问题。
我的第一印象是您正在做很多工作:为每个线程创建一个文件目标,为每个线程创建一个新规则,获取一个新的记录器实例等,您可能不需要做这些工作来完成您想要的事情来完成。
我知道 NLog 允许至少基于一些 NLog LayoutRenderer 动态命名输出文件。例如,我知道这是可行的:
并且会给你这样的文件名:
因此,例如,似乎你可以使用这样的模式来基于线程 id 创建输出文件:
如果你最终有线程 101 和 102,那么您将有两个日志文件:101.log 和 102.log。
在你的例子中,你想根据你自己的 id 来命名文件。您可以将 id 存储在 MappedDiagnosticContext (这是一个字典,允许您存储线程本地名称-值对),然后在您的模式中引用它。
您的文件名模式看起来像这样:
因此,在您的代码中您可以这样做:
像这样的东西应该允许您的 ThreadManager 类有一个名为“ThreadManager”的记录器。每次记录消息时,它都会在 Info 调用中记录格式化字符串。如果记录器配置为记录到文件目标(在配置文件中制定一条规则,将“*.ThreadManager”发送到文件目标,其文件名布局如下所示:
记录消息时,NLog 将确定什么文件名应该基于 fileName 布局的值(即,它在日志时应用格式化标记)。如果文件存在,则将消息写入其中。如果文件尚不存在,则创建该文件。 如果每个线程都有一个随机ID
,如“aaaaaaaaaaa”、“aaaaaaaaaaab”、“aaaaaaaaaaac”,那么您应该获得如下日志文件:
等等
如果您可以这样做,那么 。你的生活应该更简单,因为你不需要 NLog 的所有编程配置(创建规则和文件目标)并且你可以让 NLog 担心创建输出文件名,
我不确定这是否有效 。或者,即使确实如此,你可能确实需要从更大的角度来看你正在做的事情。测试它是否有效应该很容易(即您可以根据 MappedDiagnosticContext 中的值命名输出文件)。如果它适用于该情况,那么您可以在创建数千个线程的情况下尝试它。
更新:
这是一些示例代码:
使用此程序:
以及此 NLog.config 文件:
我能够为委托的每次执行获取一个日志文件。日志文件以 MDC (MappedDiagnosticContext) 中存储的“id”命名。
因此,当我运行示例程序时,我得到了 50 个日志文件,每个日志文件都有三行“Enter...”、“Hello...”、“Exit...”。每个文件都命名为
log__X_.txt
,其中 X 是捕获的计数器 (ii) 的值,因此我有 log_0.txt、log_1.txt、log_1.txt 等、log_49.txt。每个日志文件仅包含与委托的一次执行相关的日志消息。这与您想要做的类似吗?我的示例程序使用任务而不是线程,因为我之前已经编写过它。我认为该技术应该足够容易地适应你正在做的事情。
您也可以使用相同的 NLog.config 文件以这种方式(为委托的每次执行获取一个新的记录器):
I don't really have an answer to your problem, but I do have some observations and some questions:
According to your code, it looks like you want to create a logger per thread and you want to have that logger log to a file named for some passed-in id value. So, the logger whose id is "abc" would log to "x:\abc.log", "def" would log to "x:\def.log", and so on. I suspect that you can do this via NLog configuration rather than programmatically. I don't know if it would work any better, or if NLog would have the same issue as you are having.
My first impression is that you are doing a lot of work: creating a file target per thread, creating a new rule per thread, getting a new logger instance, etc, that you might not need to do to accomplish what it appears that you want to accomplish.
I know that NLog allows the output file to be named dynamically, based on at least some of the NLog LayoutRenderers. For example, I know that this works:
and will give you filenames like this:
So, for example, it seems that you could use a pattern like this to create output file(s) based on thread id:
And if you ended up having threads 101 and 102, then you would have two log files: 101.log and 102.log.
In your case, you want to name the file based on your own id. You could store the id in the MappedDiagnosticContext (which is a dictionary that allows you to store thread-local name-value pairs) and then reference that in your pattern.
Your pattern for your filename would look something like this:
So, in your code you might do this:
Something like this should allow your ThreadManager class to have a single logger named "ThreadManager". Each time it logs a message, it will log the formatted string in the Info call. If the logger is configured to log to the File target (in the config file make a rule that sends "*.ThreadManager" to a File target whose filename layout is something like this:
At the time a message is logged, NLog will determine what the filename should be, based on the value of the fileName layout (i.e. it applies the formatting tokens at log time). If the file exists, then the message is written to it. If the file does not exist yet, the file is created and the message is logged to it.
If each thread has a random id like "aaaaaaaaaaaa", "aaaaaaaaaaab", "aaaaaaaaaaac", then you should get log files like this:
And so on.
If you can do it this way, then your life should be simpler as you do not have to all of that programmatic configuration of NLog (creating rules and file targets). And you can let NLog worry about creating the output file names.
I don't know for sure that this will work any better than what you were doing. Or, even if it does, you might really need to what you are doing in your bigger picture. It should be easy enough to test to see that it even works (i.e. that you can name your output file based a value in the MappedDiagnosticContext). If it works for that, then you can try it for your case where you are creating thousands of threads.
UPDATE:
Here is some sample code:
Using this program:
And this NLog.config file:
I am able to get one log file for each execution of the delegate. The log file is named for the "id" stored in the MDC (MappedDiagnosticContext).
So, when I run the sample program, I get 50 log files, each of which has three lines in it "Enter...", "Hello...", "Exit...". Each file is named
log__X_.txt
where X is the value of the captured counter (ii), so I have log_0.txt, log_1.txt, log_1.txt, etc, log_49.txt. Each log file contains only those log messages pertaining to one execution of the delegate.Is this similar to what you want to do? My sample program uses Tasks rather than threads because I had already written it some time ago. I think that the technique should adapt to what you are doing easily enough.
You could also do it this way (getting a new logger for each excecution of the delegate), using the same NLog.config file:
这是一个较老的问题,但作为 NLog 的当前所有者,我有以下见解:
LoggingConfiguration.AddRule
+ReconfigExistingLoggers
时)您应该避免在运行时更改现有项目的值。相反,应该使用 上下文渲染器 (
$ {事件属性}
、${GDC}
、${MDLC}
等)This is an older question, but as current owner of NLog I have the following insights:
LoggingConfiguration.AddRule
+ReconfigExistingLoggers
)You should avoid changing the values of existing items at runtime. Instead one should use the context renderers (
${event-properties}
,${GDC}
,${MDLC}
, etc.)我不了解 NLog,但从上面的内容和 API 文档(http://nlog-project.org/help/)可以看出,只有一种静态配置。因此,如果您想仅在创建记录器(每个记录器来自不同的线程)时使用此方法向配置添加规则,则您正在编辑相同的配置对象。据我在 NLog 文档中看到的,无法为每个记录器使用单独的配置,因此这就是您需要所有规则的原因。
添加规则的最佳方法是在启动异步工作线程之前添加规则,但我假设这不是您想要的。
也可以只为所有工人使用一台记录器。但我假设您需要将每个工作人员放在一个单独的文件中。
如果每个线程都创建自己的记录器并将自己的规则添加到配置中,则必须在其周围加锁。请注意,即使您同步了代码,在您更改规则时仍然有可能其他一些代码正在枚举规则。正如您所展示的,NLog 不会对这些代码位进行锁定。所以我认为任何线程安全声明仅适用于实际的日志写入方法。
我不确定你现有的锁是做什么的,但我不认为它没有达到你的预期。因此,更改
为
“请注意,仅锁定您“拥有”的对象(即您的类私有的对象)被认为是最佳实践。这可以防止其他代码中的某些类(不符合最佳实践)锁定同一代码,从而可能导致死锁。因此,我们应该将
privateConfigLock
定义为您的类的私有。我们还应该将其设为静态,以便每个线程都能看到相同的对象引用,如下所示:I don't know NLog, but from what I can see from the pieces above and the API docs(http://nlog-project.org/help/), there is only one static configuration. So if you want to use this method to add rules to the config only when the logger is created (each from a different thread), you're editing the same configuration object. As far as I can see in the NLog docs, there is no way to use a separate configuration for each logger, so that is why you need all the rules.
The best way to add the rules would be to add the rules before you start the async workers, but I'm going to assume that that is not what you want.
It would also be possible to just use one logger for all the workers. But I'm going to assume that you need every worker in a separate file.
If each thread is creating their own logger and adding their own rules to the configuration, you'll have to put a lock around it. Note that even with you synchronizing your code, there is still a chance that some other code is enumerating over the rules while you are changing them. As you show, NLog does not do locking around these bits of code. So I presume any thread-safe claims are for the actual log-writing methods only.
I'm not sure what your existing lock does, but I don't think it is not doing what you intended. So, change
to
Note that it is considered a best practice to only lock objects that you 'own', i.e. that are private to your class. This prevents that some class in some other code (that does not adhere to the best practice) locks on the same code that may create a deadlock. So we should define
privateConfigLock
as private to your class. We should also make it static, so that every thread sees the same object reference, like so:我认为您可能没有正确使用
lock
。您正在锁定传递到您的方法中的对象,因此我认为这意味着不同的线程可能会锁定不同的对象。大多数人只是将其添加到他们的类中:
然后锁定它并保证它始终是同一个对象。
我不确定这会解决你的问题,但你现在所做的事情对我来说很奇怪。
其他人可能不同意。
I think you might not be using
lock
correctly. You are locking on objects which are passed into your method so I think this means that different threads might lock on different objects.Most people just add this to their class:
Then lock on that and be guaranteed it will always be the same object.
I'm not sure this will fix your problem, but what you are doing now seams strange to me.
Other people might disaggree.
通过枚举集合的副本,您可以解决这个问题。我对 NLog 源代码进行了以下更改,似乎解决了问题:
By enumerating over a copy of the collection you can solve this problem. I made the following change to the NLog source and it seems to fix the problem:
根据 他们的 wiki 上的文档,它们是线程安全的:
根据您收到的错误,您很可能正在执行异常所指示的操作:在 foreach 循环时修改代码中某处的集合正在迭代。由于您的列表是通过引用传递的,因此不难想象在您迭代规则集合时另一个线程将在哪里修改规则集合。您有两个选择:
rules.AsReadOnly()
),但会错过您生成的线程创建新规则时发生的任何更新。According to documentation on their wiki they are thread safe:
Based on the error you are getting you are most likely doing exactly what the exception sates: modifying the collection somewhere in the code while the foreach loop is iterating. Since your list is passed by reference this isn't hard to imagine where another thread would be modifying the collection of rules while you are iterating over it. You have two options:
rules.AsReadOnly()
) but miss any updates that happen as threads you've spawned create new rules.