VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > Python基础教程 >
  • C#教程之C# 多线程学习笔记 - 2(2)

{ public static readonly EventWaitHandle WaitHandle_MainThread = new AutoResetEvent(false); public static readonly EventWaitHandle WaitHandle_WorkThread = new AutoResetEvent(false); public static string Message = string.Empty; public static readonly object Locker = new object(); public void WorkThread() { while (true) { WaitHandle_MainThread.Set(); WaitHandle_WorkThread.WaitOne(); lock (Locker) { if (Message == null) return; Console.WriteLine($"收到主线程的消息,内容为: {Message}"); } } } }
  • 生产消费者队列的构成如下所描述的一致。

    • 构建一个队列,用于存放需要执行的工作项。
    • 如果有新的任务需要执行,将其放在队列当中。
    • 一个或多个工作线程在后台执行,从队列中拿取工作项执行,将其消费。
  • 生产/消费者队列可以精确控制工作线程的数量,CLR 的线程池就是一种生产/消费者队列。

  • 结合 AutoResetEvent 事件等待句柄,我们可以很方便地实现一个生产/消费者队列。

    class Program
    {
     static void Main(string[] args)
     {
         using (var queue = new ProducerConsumerQueue())
         {
             queue.EnqueueTask("Hello");
             for (int i = 0; i < 10; i++)
             {
                 queue.EnqueueTask($"{i}");
             }
             queue.EnqueueTask("End");
         }
     }
    }
    
    public class ProducerConsumerQueue : IDisposable
    {
     private readonly EventWaitHandle _waitHandle = new AutoResetEvent(false);
     private readonly object _locker = new object();
     private readonly Queue<string> _taskQueue = new Queue<string>();
    
     private readonly Thread _workThread;
    
     public ProducerConsumerQueue()
     {
         _workThread = new Thread(Work);
         _workThread.Start();
     }
    
     public void EnqueueTask(string task)
     {
         // 向队列当中插入任务,加锁保证线程安全
         lock (_locker)
         {
             _taskQueue.Enqueue(task);
         }
    
         // 通知工作线程开始干活
         _waitHandle.Set();
     }
    
     private void Work()
     {
         while (true)
         {
             string task = null;
             lock (_locker)
             {
                 if (_taskQueue.Count > 0)
                 {
                     task = _taskQueue.Dequeue();
                     if (task == null) return;
                 }
             }
    
             if (task != null)
             {
                 Thread.Sleep(100);
                 Console.WriteLine($"正在处理任务 {task}");
             }
             else
             {
                 // 如果任务等于空则阻塞线程,等待心的工作项
                 _waitHandle.WaitOne();
             }
         }
     }
    
     public void Dispose()
     {
         // 优雅退出
         EnqueueTask(null);
         _workThread.Join();
         _waitHandle.Close();
     }
    }
  • .NET 4.0 以后提供了一个 BlockingCollection<T> 类型实现了生产/消费者队列。

  • 4.2 ManualResetEvent

    1. 与 AutoResetEvent 类似,但在调用 Set() 方法的时候打开门,是可以允许任意数量的线程在调用 WaitOne() 后通过。(与 AutoResetEvent 每次只能通过 1 个不一样)

    2. 如果是在关闭状态下调用 WaitOne() 方法,线程会被阻塞,其余功能都与 AutoResetEvent 一致。

    3. ManualResetEvent 的基类也是 EventWaitHandle ,通过以下两种方式均可构造。

      var manual1 = new ManualResetEvent(false);
      var manual2 = new EventWaitHandle(false, EventResetModel.ManualReset);
    4. .NET 4.0 提供了性能更高的 ManualResetEventSliam ,但是不能够跨线程使用。

    4.3 CountdownEvent

    1. 使用 CountdownEvent 可以指定一个计数器的值,用于表明需要等待的线程数量。

    2. 调用 Signal() 方法会将计数器自减 1 ,如果调用其 Wait() 则会阻塞计数到 0 ,通过 AddCount() 可以增加计数。

      class Program
      {
       static void Main()
       {
           var test = new CountdownEventTest();
      
           new Thread(test.Say).Start("Hello 1");
           new Thread(test.Say).Start("Hello 2");
           new Thread(test.Say).Start("Hello 3");
      
           test.CountdownEvent.Wait();
           Console.WriteLine("所有线程执行完成...");
       }
      }
      
      public class CountdownEventTest
      {
       public readonly CountdownEvent CountdownEvent = new CountdownEvent(3);
      
       public void Say(object info)
       {
           Thread.Sleep(1000);
           Console.WriteLine(info);
           CountdownEvent.Signal();
       }
      }
    3. 当计数为 0 的时候,无法通过 AddCount() 增加计数,只能调用 Reset() 进行复位。

    4.4 等待句柄与线程池

    1. 除了手动开启线程之外,事件等待句柄也支持通过线程池来运行工作任务。

    2. 通过 ThreadPool.RegisterWaitForSingleObject() 方法可以减少资源消耗,当需要执行的委托处于等待状态的时候,不会浪费线程资源。

      class Program
      {
       static void Main()
       {
           var test = new ThreadPoolTest();
           test.Test();
       }
      }
      
      public class ThreadPoolTest
      {
       private readonly EventWaitHandle _waitHandle = new ManualResetEvent(false);
      
       public void Test()
       {
           RegisteredWaitHandle regHandle = ThreadPool.RegisterWaitForSingleObject(_waitHandle, Work, "OJBK", -1, true);
      
           Thread.Sleep(1000);
           _waitHandle.Set();
           Console.ReadLine();
           regHandle.Unregister(_waitHandle);
       }
      
       public void Work(object data,bool timeout)
       {
           Console.WriteLine($"正在执行任务 {data} .....");
       }
      }
    3. 上述代码如果通过传统的方式进行阻塞与信号发送, 那么有 1000 个请求 Work() 方法,就会造成大量服务线程阻塞,而 RegisterWaitForSingleObject 可以立即返回,不会浪费线程资源。

    4.5 跨进程的 EventWaitHandle

    可以通过对 EventWaitHandle 类型构造函数的第三个参数传入标识,来获得跨进程的事件的等待句柄。

    EventWaitHandle wh = new EventWaitHandle(false,EventResetMode.AutoReset,"AppName.Identity");

    五、同步上下文

    5.1 使用同步上下文

    1. 这里的同步上下文不是 SynchronizationContext 类,而是 CLR 的自动锁机制。
    2. 通过继承 ContextBoundObject 基类并添加 Synchronization 特性即可让 CLR 自动加锁。
    3. 同步上下文是一种比较重型的加锁方法,很容易引起死锁的情况发生。

    5.2 重入

    1. 线程安全方法也被称之为可重入的,因为其可以在运行途中被其他线程抢占。
    2. 使用了自动锁会有一个严重问题,如果将 Synchronization 特性的 reentrant 参数设置为 true 。则允许同步类是可被重入的,这就导致同步上下文被临时释放,会导致过度期间任何线程都可以自由调用原对象的任何方法。
    3. 这是因为 Synchronization 特性是直接作用于类,所以其所有方法都会带来可重入的问题。
    4. 所以因尽量减少粗粒度的自动锁。
    作者:MyZony
    声明:原创博客请在转载时保留原文链接或者在文章开头加上本人博客地址,如发现错误,欢迎批评指正。凡是转载于本人的文章,不能设置打赏功能,如有特殊需求请与本人联系!

    
    相关教程
    关于我们--广告服务--免责声明--本站帮助-友情链接--版权声明--联系我们       黑ICP备07002182号