摘自:http://www.cnblogs.com/willick/p/4177977.html 仅供参考学习
有时候你需要一个线程在接收到某个信号时,才开始执行,否则处于等待状态,这是一种基于信号的事件机制。.NET框架提供一个ManualResetEvent类来处理这类事件,它的 WaiOne 实例方法可使当前线程一直处于等待状态,直到接收到某个信号。它的Set方法用于打开发送信号。下面是一个信号机制的使用示例:
1 //线程的信号机制 2 #region 3 var signal = new ManualResetEvent(false); 4 DateTime beginTime = DateTime.Now; 5 new Thread(() => { 6 Console.WriteLine("waiting for signal..."); 7 signal.WaitOne(); 8 signal.Dispose(); 9 Console.WriteLine("Got signal"); 10 }).Start(); 11 Thread.Sleep(2000); 12 TimeSpan ts = (DateTime.Now - beginTime); 13 Console.WriteLine("已消耗啦"+ts.TotalMilliseconds); 14 signal.Set(); 15 16 Console.ReadKey(); 17 #endregion
效果:
1 waiting for signal... 2 已消耗啦2003.1145 3 Got signal
当执行Set方法后,信号保持打开状态,可通过Reset方法将其关闭,若不再需要,通过Dispose将其释放。如果预期的等待时间很短,可以用ManualResetEventSlim代替ManualResetEvent,前者在等待时间较短时性能更好。信号机制非常有用,后面的日志案例会用到它。
线程池中的线程
线程池中的线程是有CLR来管理的,在下面的2中条件下,线程池能起到最好的效用:
*任务运行的时候比较短(<250ms),这样CLR可以充分调配现有的空闲线程来处理该任务;
*大量时间处于等待(或阻塞)的任务不去支配线程池的线程。
1 // 方式1:Task.Run,.NET Framework 4.5 才有 2 Task.Run (() => Console.WriteLine ("Hello from the thread pool")); 3 4 // 方式2:ThreadPool.QueueUserWorkItem 5 ThreadPool.QueueUserWorkItem (t => Console.WriteLine ("Hello from the thread pool"));
案例:支持并发的异步日志组件
基于上面的知识,我们可以实现应用程序的并发写日志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:
- 在后台异步执行,和其它线程互不影响。
根据上文线程池的两个最优使用条件,由写日志线程会长时间处于阻塞(或运行等待)状态,所以它不适合使用线程池。即不能使用Task.Run,而最好使用new Thread。 - 支持并发,即多个任务(分布在不同线程上)可同时调用写日志功能,但需保证线程安全。
支持并发,必然要用到锁,但要完全保证线程安全,那就要想办法避免“死锁”。只要我们把“上锁”的操作始终由同一个线程来做即可避免“死锁”问题,但这样的话,并发请求的任务只能放在队列中由该线程依次执行(因为是后台执行,无需即时响应用户,所以可以这么做)。 - 单个实例,单个线程。
任何地方调用写日志功能都调用的是同一个Logger实例(显然不能每次写日志都新建一个实例),即需使用单例模式。不管有多少任务调用写日志功能,都必须始终使用同一个线程来处理这些写日志操作,以保证不占用过多的线程资源和避免新建线程带来的延迟。
1 public class Logger 2 { 3 /* 4 * 1.需要一个用来存放写日志任务的队列 5 * 2.需要有一个信号机制来标识是否有新的任务要执行 6 * 3.当有新的写日志任务时,将该任务加入到队列中,并发出信号 7 * 4.用一个方法来处理队列中的任务,当接受新任务信号时,就依次调用队列中的任务 8 * 5.lock对象要实现对入栈和出栈的锁操作,保证出栈的时候不会有入栈的操作 9 */ 10 private Queue<Action> queue; 11 private ManualResetEvent switchSignal; 12 private Thread loggingThread; 13 private static readonly Logger log = new Logger(); 14 public static Logger GetIntence() 15 { 16 return log; 17 } 18 private Logger() 19 { 20 queue = new Queue<Action>(); 21 switchSignal = new ManualResetEvent(false); 22 loggingThread = new Thread(ReceiveInfo); 23 loggingThread.IsBackground = true; 24 loggingThread.Start(); 25 } 26 private void ReceiveInfo() 27 { 28 switchSignal.WaitOne(); 29 switchSignal.Reset(); 30 31 Thread.Sleep(100); 32 Queue<Action> oldQueue; 33 lock (queue) 34 { 35 oldQueue = new Queue<Action>(queue); 36 queue.Clear(); 37 } 38 foreach (var action in oldQueue) 39 { 40 action(); 41 } 42 } 43 //任务添加 44 public void WriteLog(string content) 45 { 46 lock (queue)//存在线程安全问题,可能发生阻塞。 47 { 48 queue.Enqueue(() => File.AppendAllText("log.txt", content)); 49 } 50 switchSignal.Set(); 51 } 52 public static void BeginLog(string content) 53 { 54 Task.Factory.StartNew(() => GetIntence().WriteLog(content));//4.0 不支持task.run(); 55 } 56 }
1 static void Main(string[] args) 2 { 3 Thread t1 = new Thread(Working); 4 t1.Name = "Thread1"; 5 Thread t2 = new Thread(Working); 6 t2.Name = "Thread2"; 7 Thread t3 = new Thread(Working); 8 t3.Name = "Thread3"; 9 10 // 依次启动3个线程。 11 t1.Start(); 12 t2.Start(); 13 t3.Start(); 14 15 Console.ReadKey(); 16 } 17 18 // 每个线程都同时在工作 19 static void Working() 20 { 21 // 模拟1000次写日志操作 22 for (int i = 0; i < 1000; i++) 23 { 24 // 异步写文件 25 Logger.BeginLog(Thread.CurrentThread.Name + " writes a log: " + i + ", on " + DateTime.Now.ToString() + " "); 26 } 27 } 28 }
通过这个示例,目的是让大家掌握线程和并发在开发中的基本应用和要注意的问题。
遗憾的是这个Logger类并不完美,而且存在线程安全问题(代码中用红色字体标出),虽然实际环境概率很小。可能上面代码多次运行都很难看到有异常发生(我多次运行未发生异常),但同时再添加几个线程可能就会有问题了。