【问题标题】:How to get background logger class to finish logging when application exits. (C# multi-threading)如何在应用程序退出时让后台记录器类完成记录。 (C#多线程)
【发布时间】:2017-01-11 04:56:09
【问题描述】:

我是 C# 编程的新手,只是为了学习,我想制作一个在低优先级后台线程上运行的异步应用程序记录器。我编写了一个记录器类,用作应用程序中任何其他类的接口,以创建新的日志条目。然后这个记录器简单地将它发送到一个日志调度器,然后它把它推送到一个 BlockingCollection,一个循环不断地尝试从后台线程中获取日志并写入日志文件。

但是,我遇到的问题是,如果应用程序在退出之前请求日志条目,则永远不会写入日志。 LogDispatcher 的 Dispose 方法也永远不会被调用,因为该方法中的退出日志永远不会被写入。我不确定如何让后台线程在它终止之前完成整个阻塞集合,然后在父应用程序线程终止时调用 dispose 方法。这是代码。

 public sealed class Logger : ILogger
 {
    // Fully lazy implementation for 
    // 1) learning purposes and
    // 2) in case log disabling is a feature to be added in the future
    private static readonly Lazy<Logger> L = new Lazy<Logger>(() => new Logger());

    public static Logger Instance
    {
        get
        {
            return L.Value;
        }
    }

    private double entryNumber = 0;
    private int verbosity { get; set; }`private Logger()
    {
        this.verbosity = 3;
        LogDispatcher.Instance.pushNewLog(new Log(
            Log.LogType.Debug,
            entryNumber++,
            "LOG -- LogFile created with verbosity " + this.verbosity,
            DateTime.Now.ToString("yyyy - mm - dd hh: mm:ss.fff")));
    }

    public void debug(string message)
    {
        if (this.verbosity > 2)
        {
            LogDispatcher.Instance.pushNewLog(new Log(
                Log.LogType.Debug,
                entryNumber++,
                message,
                DateTime.Now.ToString("yyyy-mm-dd hh:mm:ss.fff"
            )));
        }
    }

    public void warning(string warning)
    {
        if (this.verbosity > 1)
        {
            LogDispatcher.Instance.pushNewLog(new Log(
                Log.LogType.Warning,
                entryNumber++,
                warning,
                DateTime.Now.ToString("yyyy-mm-dd hh:mm:ss.fff"
            )));
        }
    }

    public void exception(string message, Exception e)
    {
        LogDispatcher.Instance.pushNewLog(new Log(
            Log.LogType.Exception,
            (entryNumber++),
            message,
            DateTime.Now.ToString("yyyy-mm-dd hh:mm:ss.fff"),
            Thread.CurrentThread.ManagedThreadId,
            e
        ));
    }

dispatcher的代码如下

internal class LogDispatcher : IDisposable
{
    private static readonly Lazy<LogDispatcher> LD = new Lazy<LogDispatcher>(() => new LogDispatcher());

    private readonly StreamWriter LogFile;

    private readonly BlockingCollection<Log> logQueue;

    private Thread loggingThread;

    private bool terminate = false;

    public static LogDispatcher Instance
    {
        get
        {
            return LD.Value;
        }
    }private LogDispatcher()
    {
        // initialize the log folder
        string logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\NETLogger";
        {
            try
            {
                Directory.CreateDirectory(logPath);
            }
            catch (Exception e)
            {
                Console.WriteLine("An error occured while making the logfile.");
                Console.WriteLine(e.ToString());
            }
        }

        // now make logfile
        try
        {
            this.LogFile = File.CreateText(logPath + "\\LogFile.txt");
            this.LogFile.AutoFlush = true;
        }
        catch (Exception e)
        {
            Console.WriteLine("An error occured while opening LogFile.txt");
            Console.WriteLine(e.ToString());
        }

        // make the blocking collection that stores all queued logs
        this.logQueue = new BlockingCollection<Log>();

        // initialize writer loggingThread to run in the background
        this.loggingThread = new Thread(new ThreadStart(logWriter));
        this.loggingThread.IsBackground = true;
        this.loggingThread.Start();
    }

    public void logWriter()
    {
        Log writingLog = new Log();
        while (!terminate && logQueue.Count != 0)
        {
            if (this.logQueue.TryTake(out writingLog, -1))
            {
                switch(writingLog.lType)
                {
                    case (Log.LogType.Debug) :
                        LogFile.WriteLine(writingLog.entryNumber + " -- DEBUG -- " + writingLog.timeStamp);
                        LogFile.WriteLine("\t" + writingLog.message);
                        break;

                    case (Log.LogType.Warning) :
                        LogFile.WriteLine(writingLog.entryNumber + " -- WARNING -- " + writingLog.timeStamp);
                        LogFile.WriteLine("\t" + writingLog.message);
                        break;

                    case (Log.LogType.Exception) :
                        LogFile.WriteLine(writingLog.entryNumber + " -- EXCEPTION -- " + writingLog.timeStamp);
                        LogFile.WriteLine("\t ============== EXCEPTION INFORMATION FOLLOWS ============== ");
                        LogFile.WriteLine("\t Log Message : " + writingLog.message);
                        LogFile.WriteLine("\t Exception Message: " + writingLog.e.Message);
                        LogFile.WriteLine("\t Exception Thread :" + writingLog.threadId);
                        LogFile.WriteLine("\t Source: " + writingLog.e.ToString());
                        LogFile.WriteLine("\t=============== END EXCEPTION INFORMATION ================== ");
                        break;
                }
            }
        }
    }

   public void pushNewLog(Log log)
    {
        logQueue.Add(log);
    }

    public void Dispose()
    {
        this.logQueue.Add(new Log(
            Log.LogType.Debug,
            -1,
            "Terminating All Logging Procedures",
            DateTime.Now.ToString("yyyy-mm-dd hh:mm:ss.fff")));
        this.logQueue.CompleteAdding();
        this.terminate = true;
        this.loggingThread.Join();
    }

    ~LogDispatcher()
    {
        this.Dispose();
    }

【问题讨论】:

    标签: c# .net multithreading logging


    【解决方案1】:

    要么使用Thread.Join 方法,要么通过设置IsBackground = false; 将您的线程标记为前台线程。

    来自MSDN

    线程要么是后台线程,要么是前台线程。后台线程与前台线程相同,只是后台线程不会阻止进程终止。一旦属于一个进程的所有前台线程都已终止,公共语言运行时就会结束该进程。任何剩余的后台线程都将停止并且不会完成。

    我知道您可能正在学习线程,但如果不是这样,请使用更高的抽象,这样您就不必直接处理线程。像这样使用Task

    Task task1 = Task.Factory.StartNew(() => DoSomething());
    Task task2 = Task.Factory.StartNew(() => DoSomethingElse());
    Task.WaitAll(task1, task2);
    Console.WriteLine("All threads complete");
    

    编辑

    OP 在评论中问:为什么调用调度程序的析构函数时不调用 dispose 方法?

    它没有被调用,因为你需要调用它。请参阅下面的模式了解如何执行此操作。

    public class ComplexResourceHolder : IDisposable
    {
    
        private IntPtr buffer; // unmanaged memory buffer
        private SafeHandle resource; // disposable handle to a resource
    
        public ComplexResourceHolder()
        {
            this.buffer = ... // allocates memory
            this.resource = ... // allocates the resource
        }
    
        // disposing will be false when it is called from the finalize. 
        // I remember this by telling myself: "finalize has 'f' in it 
        // and false has 'f' in it so disposing is false when called by finalizer".
        protected virtual void Dispose(bool disposing)
        {
            ReleaseBuffer(buffer); // release unmanaged memory
            if (disposing)
            { // release other disposable objects
                if (resource != null) resource.Dispose();
            }
        }
    
        ~ComplexResourceHolder()
        {
            Dispose(false);
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
    

    X 避免使类型可终结。 仔细考虑您认为需要终结器的任何情况。从性能和代码复杂性的角度来看,带有终结器的实例存在实际成本。在可能的情况下,更喜欢使用 SafeHandle 等资源包装器来封装非托管资源,在这种情况下,终结器变得不必要,因为包装器负责自己的资源清理。

    X 不要使值类型最终化。 只有引用类型实际上由 CLR 完成,因此任何在值类型上放置终结器的尝试都将被忽略。 C# 和 C++ 编译器强制执行此规则。

    ✓ 如果类型负责释放没有自己的终结器的非托管资源,请务必使该类型可终结。

    【讨论】:

    • IsBackground = false?
    • @CodingYoshi 非常感谢您的回答。是的,我正在尝试学习线程,因此手动线程管理。我的后续问题是,如果将线程设为前台线程,它是否与运行主应用程序的线程相同。它会减慢其他进程并让程序等待日志写入完成吗?
    • @CodingYoshi 你能告诉我为什么调用调度程序的析构函数时没有调用 dispose 方法吗?是不是因为线程在后台过早终止?
    • @Vector 制作线程前台线程仍然是线程。不,它与主线程不同。析构函数不会调用您的 Dispose 方法,因为您没有调用它。请参阅编辑。
    • @CodingYoshi 再次感谢。最后一个问题。在这种情况下,让调度程序实现 IDisposable 更好,还是应该在析构函数中创建一个最终退出日志条目,并让 GC 在线程结束时手动解构该类,因为这将降低代码复杂性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-20
    • 1970-01-01
    • 1970-01-01
    • 2010-12-09
    • 2021-12-03
    相关资源
    最近更新 更多