在任何情况下BlockingCollection线程都是安全的吗?
我正在尝试使用BlockingCollection来实现日志系统,而不需要占用太多的主线程资源。这个想法是它只花费主线程的BlockingCollection.Add()调用,并且一个等待任务执行写作。
BlockingCollection类应该用它的队列来处理任务竞争,这样每个Add()都会连续带来一个工作。
声明BlockingCollection对象:
private static BlockingCollection<string> queueLogMinimal = new BlockingCollection<string>();
其他变种:
internal static string AllLogFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Log.log");
private static Object logLocker = new Object();
在构造函数中启动将始终侦听的任务:
public LogManager()
{
Task.Factory.StartNew(() =>
{
Thread.CurrentThread.Name = "Logger";
foreach (string message in queueLogMinimal.GetConsumingEnumerable())
{
WriteLogMinimal(message);
}
});
}
调用以管理日志记录的最小函数:
public static void LogMinimal(string message)
{
queueLogMinimal.Add(message);
}
需要锁定,以确保在Logger线程写入时主线程不会更改文件配置。
每个BlockingCollection的任务调用的函数出列:
public static void WriteLogMinimal(string message)
{
lock (logLocker)
{
try
{
File.AppendAllText(AllLogFileName, DateTime.Now.ToString() + " - " +message +Environment.NewLine);
}
catch (Exception e)
{
Debug.WriteLine("EXCEPTION while loging the message : ");
}
}
}
事情是,通过连续记录测试该系统的功能。如果我测试,请说连续40个日志:
[TestClass]
public class UnitTest
{
[TestMethod]
public void TestLogs()
{
LogManager lm = new LogManager();
int a = 0;
LogManager.LogMinimal(a++.ToString());
LogManager.LogMinimal(a++.ToString());
LogManager.LogMinimal(a++.ToString());
LogManager.LogMinimal(a++.ToString());
}
}
在 10到30个日志之后,它会停止日志记录,因为这发生在LaunchLogTask()中,在foreach中使用GetConsumingEnumerable()浏览BlockingCollection元素:
线程正在中止。
StackTrace:
at System.Threading.Monitor.ObjWait(布尔值exitContext,Int32毫秒时间,对象obj)\ r \ n
at System.Threading.Monitor.Wait(Object obj,Int32 millisecondsTimeout,Boolean exitContext)\ r \ n
at System.Threading.SemaphoreSlim.WaitUntilCountOrTimeout(Int32 millisecondsTimeout,UInt32 startTime,CancellationToken cancellationToken)\ r \ n at System.Threading.SemaphoreSlim.Wait(Int32 millisecondsTimeout,CancellationToken cancellationToken)\ r \ n
at System.Collections.Concurrent.BlockingCollection`1.TryTakeWithNoTimeValidation(T&amp; item,Int32 millisecondsTimeout,CancellationToken cancellationToken,CancellationTokenSource combinedTokenSource)\ r \ n
at System.Collections.Concurrent.BlockingCollection`1.d__68.MoveNext()\ r \ n
在RenaultTrucks.Common.Managers.LogManager。&lt;&gt; c。&lt; .ctor&gt; b__14_1() 在 C:\ Source \ Diagnostica \ GLB-DIAGNOSTICA \ NewDiagnostica \ Source \ Common \ Managers \ LogManager.cs:第61行&#34;串
如果我在增加睡眠时间的同时进行较少的测试:
[TestClass]
public class UnitTest
{
[TestMethod]
public void TestLogs()
{
LogManager lm = new LogManager();
int a = 0;
LogManager.LogMinimal(a++.ToString());
System.Threading.Thread.Sleep(100);
LogManager.LogMinimal(a++.ToString());
System.Threading.Thread.Sleep(100);
LogManager.LogMinimal(a++.ToString());
System.Threading.Thread.Sleep(100);
LogManager.LogMinimal(a++.ToString());
}
}
记录一切! (没有线程流产)。
是否可能根据排队速度对队列进行不同的管理,或者该速度可能过快&#34;吗
答案 0 :(得分:1)
问题在于,您不会在退出单元测试之前等待处理所有日志消息。
Task.Factory.StartNew
(顺便打算使用哪种用法)在后台线程池线程上运行你的代码。当您的进程即将退出时 - 所有后台线程都将中止,这是您观察到的异常。
首先,对于这种长时间运行的操作使用线程池线程并不好。最好为此启动新的后台线程,并保存在变量中,因为我们稍后会需要它:
private readonly Thread _consumerThread;
public LogManager() {
new Thread(() => {
Thread.CurrentThread.Name = "Logger";
try {
foreach (string message in queueLogMinimal.GetConsumingEnumerable()) {
try {
LogMinimal(message);
}
catch (Exception ex) {
// do something, otherwise one failure to write a log
// will bring down your whole logging
}
}
}
catch (Exception ex) {
// do something, don't let exceptions go unnoticed
}
}) {
IsBackground = true
}.Start();
}
现在,在退出流程之前,您需要处理所有待处理的消息。合理的做法是实施IDisposable
,但这不是必需的,你只需要一些你在记录器上调用的方法来通知它应该停止。
public void Dispose() {
// first notify that no messages are expected any more
// this will allow GetConsumerEnumerable to finish
this.queueLogMinimal.CompleteAdding();
// now wait for thread to finish with reasonable timeout
// don't do this without timeout to prevent potential deadlocks
bool finishedGracefully = _consumerThread.Join(TimeSpan.FromSeconds(5));
if (!finishedGracefully) {
// thread did not finish during timeout,
// do something like throwing exception
}
}
现在告诉您的记录器在退出前停止:
[TestMethod]
public void TestLogs()
{
LogManager lm = new LogManager();
int a = 0;
lm.LogMinimal(a++.ToString());
// ...
lm.Dispose();
}
最后注意事项:不要自己实施记录器,只需使用已经为您完成所有操作的现有解决方案,例如log4net。
答案 1 :(得分:0)
我认为这里的图片与您认为的没什么不同。
基本上,您的线程不在等待日志记录,请注意线程数将根据您的情况不断增加。
原因:Task.Factory.StartNew
这使您所有的新线程都可以处理blockingcollection.GetConsumingEnumerables。.
您未记录日志的情况是线程耗尽。请验证。
建议不要使用StartNew,而是使用线程类并运行它。
感谢阅读