C# BlockingCollection文件日志记录-BlockingCollection任务的线程自动中止

C# BlockingCollection文件日志记录-BlockingCollection任务的线程自动中止,c#,multithreading,logging,task,blockingcollection,C#,Multithreading,Logging,Task,Blockingcollection,BlockingCollection线程在任何情况下都是安全的吗? 我试图使用BlockingCollection来实现日志系统,而不占用太多的主线程资源。其思想是只需对主线程调用BlockingCollection.Add(),一个等待的任务就可以完成编写工作 BlockingCollection类应该负责处理与其队列之间的任务竞争,这样每个Add()都会依次产生一个作业 声明BlockingCollection对象: private static BlockingCollection<

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 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 : ");
            }
        }
    }
在构造函数中启动将始终侦听的任务:

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 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 : ");
            }
        }
    }
为管理日志而调用的最小函数:

    public static void LogMinimal(string message)
    {
        queueLogMinimal.Add(message);
    }
锁定是必需的,以确保主线程在记录器线程写入时不会更改文件配置

任务在每次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 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:

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 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 : ");
            }
        }
    }
atSystem.Threading.Monitor.ObjWait(布尔exitContext,Int32毫秒超时,对象obj)\r\n
atSystem.Threading.Monitor.Wait(对象对象对象,Int32毫秒超时,布尔exitContext)\r\n
在System.Threading.SemaphoreSlim.WaitUntillCountertimeout(Int32毫秒计时,UInt32开始时间,CancellationToken CancellationToken)\r\n在System.Threading.SemaphoreSlim.Wait(Int32毫秒计时,CancellationToken CancellationToken)\r\n
atSystem.Collections.Concurrent.BlockingCollection`1.TryTakeWithNoTimeValidation(T&item,Int32毫秒计时,取消令牌取消令牌,取消令牌源合并令牌源)\r\n
atSystem.Collections.Concurrent.BlockingCollection`1.d\u 68.MoveNext()\r\n
RenaultTrucks.Common.Managers.LogManager.c.b_u14_u1() C:\Source\Diagnostica\GLB-Diagnostica\NewDiagnostica\Source\Common\Managers\LogManager.cs中:第61行“字符串

如果我在增加睡眠时间的同时测试的强度更低:

[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());
    }
}
它记录所有内容!(没有线程中止)


是否可能根据队列速度对队列进行不同的管理,或者该速度可能“太快”

这里的问题是,在退出单元测试之前,您没有等待处理所有日志消息

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();
}

最后一点注意:不要自己实现logger,只需使用已经为您实现了所有这些功能的现有解决方案,如log4net。

我认为这里的情况与您所认为的略有不同

基本上,您的线程没有等待日志记录,请注意,在您的情况下,它将不断增加线程数

原因:Task.Factory.StartNew

这将使所有新线程处理blockingcollection.GetConsumingEnumerables

您未记录的情况是线程耗尽。请验证

正如建议的那样,不要使用StartNew,而是使用thread类并运行它


感谢阅读

您能发布异常的完整堆栈跟踪吗?如果您能提供一个我们可以复制并粘贴到控制台应用程序中以重新复制您看到的内容的应用程序,那将是非常棒的。看起来您正在控制台应用程序中测试该应用程序,不要等待所有消息被处理,直到退出所述应用程序。线程安全的集合不会自动使使用它的代码也成为线程安全的。否则,一个简单的问题是,您的测试停止运行,并使单元测试运行程序中止任何剩余的线程。这会在消费线程之前关闭消费线程。这在现实生活中也会发生,任务使用后台线程,您可以LongRunning需要一个线程。使用一个线程来记录,使用Field.AppDeldTrand,并不能看到在硬崩溃分数(150个高雅点)上记录的任何东西,请考虑使用一个库。这确实是个问题,CLR也会粗暴地中止该线程,因为它的IsSead属性被设置为true。因此,您将丢失日志记录信息。这不是一件容易注意到的事情,除非它真的很重要,因为你的程序崩溃了,而且没有日志记录信息,因为线程在写文件之前就被中止了。这种东西必须通过艰苦的方式学习,而且以前写日志库的人也学过。