最近博主在做服务程序开发的时候,用的是控制台的非图形图像的界面。然后采用了log4net作为日志打印组件,在配置文件里面做了一些配置项。在控制台输出的时候,针对不同的日志级别,定义了不同的文字颜色在控制台中进行字符串输出。然后博主联想到winform程序也存在界面上的日志打印需求,通常情况下,程序员会调用系统自带的文本控件做日志打印。比如TextBox、RichTextBox等等。使用此类控件作为日志打印控件的时候,需要考虑一些细节问题,常见的有跨UI线程访问等等。既然脑海中已经产生了这样的需求,何不自己造一个呢?
日志模块应该是项目中常见或者必不可少的通用模块之一了,因为日志记录能够提升程序的可维护性。嗯,是的,没错,日志记录往往是软件开发周期中的重要组成部分。它具有以下几个优点:记录一些用来作为“依据”的重要信息,记录应用程序运行时的精确环境,记录捕获到的异常错误信息,方便开发人员尽快找到应用程序中的Bug等等。一旦在程序中加入了日志输出代码,程序运行过程中就能生成并输出日志信息而无需人工干预。
1、文本的输出格式
做日志打印的时候,针对文本输出可能需要诸如线程信息,当前进程,当前类和方法,执行时间,堆栈信息等等。针对界面输出可能需要用到不同颜色来区分日志级别。于是博主根据日志的不同级别,在控件里面自定义了几个相关属性用来控制文本的显示(具体代码看步骤6)。开发人员既可以在设计时可视化设置属性值,也可以硬编码赋值。
2、保持界面操作流畅性
UI线程大量的调用控件可能造成的界面假死,博主觉得解决这个问题的最好方式是多个线程协同分工,一方面提高程序的界面流畅性,一方面提高程序的响应速度和处理能力。UI主线程只负责从界面接收用户的操作,后台线程则负责响应该操作。比如一个查询动作,用户鼠标点击查询按钮,UI线程接收该点击事件,通知后台线程对该点击事件作出一系列包办。后台线程静悄悄的在后台查询数据,然后处理数据,然后批量加载数据并更新到界面的展示控件。
3、非UI线程访问所带来的异常
众所周知,winform窗体上的控件是由UI线程创建的,如果要进行跨UI线程访问该控件,则会导致异常(InvalidOperationException)。
4、日志打印是否有序
开发基于多线程程序的时候,如果开启VS调试,则调试起来相当麻烦,因为执行顺序会在多个线程间切换,所以调试的时候,会到处乱跳。所以一般我们会用日志记录的方式来捕获开发人员想要的程序运行信息。那么既然这些信息有利于开发人员解决bug、优化系统,则必然得保证日志产生的有序性,这样开发人员才知道,哪行代码先运行,哪个bug先出现。外部线程生产日志,内部线程消费打印日志,于是就可以引入队列来作为待处理日志的存储容器。
5、多线程的并发访问是否安全
由于多个线程同时并发调用控件的打印方法,其内部处理逻辑就是多个外部线程写入内部队列,控件内部的处理线程读取队列,那么这时候就得考虑线程安全的问题了。通常对于线程安全的问题,最好是将代码段的执行封装成原子操作。从而在源头上规避了因多个线程之间的切换而导致该代码段的执行结果存在二义性,也就是说我们不用考虑共享资源同步的问题。构建原子一般采用加锁的方式,当然也可以 lock free 。
6、编码实现
考虑到兼容性,博主决定在.Net2.0下实现这个日志打印控件。
留邮箱求源码刷评论什么的真的好么?直接贴上全部的源码和测试代码,不要叫我活雷锋,我想静静...O(∩_∩)O
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Windows.Forms; namespace WindowsForms20 { /// <summary> /// 线程安全的日志打印控件 /// Create by 陌城 /// http://www.cnblogs.com/StrangeCity/p/4356009.html /// </summary> [Description("线程安全的日志打印控件")] public class PrintLog : RichTextBox { public delegate void Action(); private readonly object _lockObj = new object(); private Queue<Action> _queueAction; private Thread _threadQueueHnadler; private AutoResetEvent _AutoResetEvent = new AutoResetEvent(true); [Browsable(false)] public new bool ReadOnly { get; private set; } [Category("外观"), Description("普通信息的文本颜色")] public Color InfoFontColor { get; set; } [Category("外观"), Description("警告信息的文本颜色")] public Color WarnFontColor { get; set; } [Category("外观"), Description("错误信息的文本颜色")] public Color ErrorFontColor { get; set; } [Category("外观"), Description("成功信息的文本颜色")] public Color SucceeFontColor { get; set; } public PrintLog() : base() { this.BackColor = Color.Black; base.ReadOnly = true; this.BorderStyle = BorderStyle.None; this.InfoFontColor = Color.White; this.WarnFontColor = Color.Yellow; this.ErrorFontColor = Color.Red; this.SucceeFontColor = Color.LightGreen; this._AutoResetEvent = new AutoResetEvent(true); this._queueAction = new Queue<Action>(); this._threadQueueHnadler = new Thread(() => { while (true) { if (this._queueAction.Count > 0) { lock (this._lockObj) { Action action = this._queueAction.Dequeue(); if (action != null) { action(); } } } else { this._AutoResetEvent.WaitOne(); } } }); this._threadQueueHnadler.IsBackground = true; this._threadQueueHnadler.Start(); } private void PrintMsg(string msg, Color fontColor) { msg = string.Format("{0}{1}{2}{1}{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), Environment.NewLine, msg); int p1 = this.TextLength; this.AppendText(msg); int p2 = msg.Length; this.Select(p1, p2); this.SelectionColor = fontColor; this.ScrollToCaret(); } private void Enqueue(string msg, Color fontColor) { lock (this._lockObj) { this._queueAction.Enqueue(() => { if (this.InvokeRequired) { this.Invoke(new MethodInvoker(() => { this.PrintMsg(msg, fontColor); })); } else { this.PrintMsg(msg, fontColor); } }); this._AutoResetEvent.Set(); } } public void Info(string msg) { this.Enqueue(msg, this.InfoFontColor); } public void Warn(string msg) { this.Enqueue(msg, this.WarnFontColor); } public void Error(string msg) { this.Enqueue(msg, this.ErrorFontColor); } public void Error(Exception ex) { this.Error(ex.ToString()); } public void Succee(string msg) { this.Enqueue(msg, this.SucceeFontColor); } } }
7、测试效果
private void button1_Click(object sender, EventArgs e) { this.printLog1.Clear(); //普通打印 for (int i = 0; i < 10000; i++) { this.printLog1.Error("sjdbnvkjsndvjn死到那时"); } return; //多线程并发打印 for (int i = 0; i < 1; i++) { Thread t = new Thread(() => { for (int j = 0; j < 10000; j++) { this.printLog1.Error("sjdbnvkjsndvjn死到那时" + i.ToString()); } }); t.Start(); } }
8、扩展思考
博主也是临时造轮子,可能考虑并不周全。例如控件是否需要增加AutoSaveToFile属性?日志内容保存到文件的功能是否可以与log4net等日志组件衔接起来,并支持适配器模式。日志控件显示了多少条日志内容应该自动清除?博主一时半会就只想到这么多了,还请各位读者多多指教!