• 线程的信号机制


    摘自: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"));

     案例:支持并发的异步日志组件

    基于上面的知识,我们可以实现应用程序的并发写日志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:

      1. 在后台异步执行,和其它线程互不影响。
        根据上文线程池的两个最优使用条件,由写日志线程会长时间处于阻塞(或运行等待)状态,所以它不适合使用线程池。即不能使用Task.Run,而最好使用new Thread。

      2. 支持并发,即多个任务(分布在不同线程上)可同时调用写日志功能,但需保证线程安全。
        支持并发,必然要用到锁,但要完全保证线程安全,那就要想办法避免“死锁”。只要我们把“上锁”的操作始终由同一个线程来做即可避免“死锁”问题,但这样的话,并发请求的任务只能放在队列中由该线程依次执行(因为是后台执行,无需即时响应用户,所以可以这么做)。

      3. 单个实例,单个线程。
        任何地方调用写日志功能都调用的是同一个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类并不完美,而且存在线程安全问题(代码中用红色字体标出),虽然实际环境概率很小。可能上面代码多次运行都很难看到有异常发生(我多次运行未发生异常),但同时再添加几个线程可能就会有问题了。

  • 相关阅读:
    bzoj 2730: [HNOI2012]矿场搭建
    bzoj 1179: [Apio2009]Atm
    strcpy,strlen, strcat, strcmp函数,strlen函数和sizeof的区别
    C语言printf的格式
    C语言中交换两个数值的方法
    C语言中 指针的基础知识总结, 指针数组的理解
    自定义方法实现strcpy,strlen, strcat, strcmp函数,了解及实现原理
    选择排序
    冒泡排序的优化
    storyBoard中取消键盘第一响应
  • 原文地址:https://www.cnblogs.com/meiCode/p/4700123.html
Copyright © 2020-2023  润新知