• 线程系列09,线程的等待、通知,以及手动控制线程数量



    当一个线程直到收到另一个线程的通知才执行相关的动作,这时候,就可以考虑使用"事件等待句柄(Event Wait Handles)"。使用"事件等待句柄"主要用到3个类: AutoResetEvent, ManualResetEvent以及CountdownEvent(.NET 4.0以后才有)。本篇包括:

     

    一个线程等待另一个线程的通知
    2个线程互相通知等待
    一个线程等待队列中的多个任务通知
    手动控制线程的数量


    □ 一个线程等待另一个线程的通知

     

    最简单的情景是:发出信号的线程只发出一次通知,等待的线程收到通知也只做一次事情。等待的线程肯定有一个等待方法,发出信号的线程必须有一个发出信号的方法,AutoResetEvent类提供了相关方法。

        class Program
    
        {
    
            //true表示将初始状态设置为终止状态
    
            static EventWaitHandle _wait = new AutoResetEvent(false);
    
            static void Main(string[] args)
    
            {
    
                new Thread(Waiter).Start();
    
                Thread.Sleep(1000);
    
                _wait.Set();//发出指示
    
            }
    
            static void Waiter()
    
            {
    
                Console.WriteLine("一切准备就绪,等待指示!");
    
                _wait.WaitOne();
    
                Console.WriteLine("收到指示~~");
    
            }
    
        }
    

    42

    ○ AutoResetEvent就像地铁入口的十字转门,有票插入,就让进,而且每次只让一个人进。
    ○ 当调用WaitOne方法,表示该线程已被阻塞,正在等待信号,就像十字转门旁等待进入的乘客。
    ○ 当调用Set方法,表示发出信号给等待线程,就像十字转门收到车票,乘客可以通过。

     

    关于AutoResetEvent:
    ○ 还可通过这种方式创建AutoResetEvent实例:var auto = new EventWaitHandle(false, EventResetMode.AutoReset);
    ○ 如果调用了Set方法,却没有其它线程调用WaitOne方法,这个handle会一直存在
    ○ 如果调用Set方法多次,却有多个线程调用WaitOne方法,也只能让这些线程挨个接收信号,即每次只有一个线程接收信号
    ○ WaitOne还有几个接收时间间隔参数的重载方法,使用WaitOne(0)可以测试一个wait handle是否已经打开
    ○ GC自动回收wait handles

     

    □ 2个线程互相通知等待

     

    还有一种情形:发出信号的线程要发出多次通知,每一次需要确认等待线程收到后再发下一个通知。大概的过程就是:线程A第一次做事并发出通知,进入等待状态;线程B收到通知,发出通知,通知线程A,线程B进入等待状态;线程A收到线程B的通知,第二次做事并发出通知,进入等待状态......2个线程互相通知,每个线程既是发出信号者,也是等待者。借助AutoResetEvent类可以解决此需求。

        class Program
    
        {
    
            static EventWaitHandle _ready = new AutoResetEvent(false);
    
            static EventWaitHandle _go = new AutoResetEvent(false);
    
            static readonly object o = new object();
    
            private static string _msg;
    
            static void Main(string[] args)
    
            {
    
                new Thread(DoSth).Start();
    
                //第一次等待直到另外一个线程准备好
    
                _ready.WaitOne();
    
                lock (o)
    
                {
    
                    _msg = "你好";
    
                }
    
                _go.Set();
    
                //第二次等待
    
                _ready.WaitOne();
    
                lock (o)
    
                {
    
                    _msg = "";
    
                }
    
                _go.Set();
    
                //第三次
    
                _ready.WaitOne();
    
                lock (o)
    
                {
    
                    _msg = null;
    
                }
    
                _go.Set();
    
            }
    
            static void DoSth()
    
            {
    
                while (true)
    
                {
    
                    _ready.Set();
    
                    _go.WaitOne();
    
                    lock (o)
    
                    {
    
                        if(_msg == null) return;
    
                        Console.WriteLine(_msg);
    
                    }
    
                }
    
            }
    
        }
    

    43

     

    把Main方法中的线程称为主线程,把另一个线程称为工作线程,2个线程是这样工作的:

    主线程使用WaitOne方法第一次等待,说:“工作线程,我等在这里”

     

    工作线程使用Set方法,说:“主线程,我给你信号,你准备第一条信息吧”,并且又使用WaitOne方法让自己等待,就说:“主线程,我给你信号了,我等在这里,准备接收你的第一条信息”,再看看暂时还没有需要显示的信息,于是作罢

     

    主线程收到工作线程的信号,设置第一条信息,然后使用Set方法,说"工作线程,我的第一条信息给你,给你信号",并且又使用WaitOne方法让自己第二次等待,说:"工作线程,我给你信号了,我等在这里"

     

    工作线程又使用Set方法,说:“主线程,我给你信号,你去准备第二条信息吧”,并且又使用WaitOne方法让自己等待,就说:“主线程,我已经给你信号了,我等在这里,准备接收你的第二条信息”,再看看这时有需要显示的信息,就把信息打印了出来

     

    依次类推


    □ 一个线程等待队列中的多个任务通知

     

    当一个等待的线程,需要逐个执行多个任务,就可以把任务放在队列中。

     

    通常把能实现实现上述需求的叫做"生产/消费队列"。所谓的"生产"是指能把多个任务放到队列中,所谓"消费"是指当任务逐一出列,再执行该任务。

        class ProducerConsumerQueue : IDisposable
    
        {
    
            EventWaitHandle _ewh = new AutoResetEvent(false);
    
            private Thread _worker; //等待线程
    
            private readonly object _locker = new object();
    
            Queue<string> _tasks = new Queue<string>();//任务队列
    
            public ProducerConsumerQueue()
    
            {
    
                _worker = new Thread(Work);
    
                _worker.Start();
    
            }
    
            //任务进入队列
    
            public void EnqueueTask(string task)
    
            {
    
                lock (_locker)
    
                {
    
                    _tasks.Enqueue(task);
    
                }
    
                //任务一旦进入队列就发出信号
    
                _ewh.Set();
    
            }
    
            void Work()
    
            {
    
                while (true)
    
                {
    
                    //从队列中获取task
    
                    string task = null;
    
                    lock (_locker)
    
                    {
    
                        if (_tasks.Count > 0)
    
                        {
    
                            task = _tasks.Dequeue();
    
                            if(task == null) return;
    
                        }
    
                    }
    
                    //如果task不为null,模拟执行task
    
                    if (task != null)
    
                    {
    
                        Console.WriteLine("正在执行线程任务 " + task);
    
                        Thread.Sleep(1000); //模拟线程执行的过程
    
                    }
    
                    else//如果taks为null
    
                    {
    
                        _ewh.WaitOne();//等待信号
    
                    }
    
                }
    
            }
    
            public void Dispose()
    
            {
    
                EnqueueTask(null); //发出信号让消费线程退出
    
                _worker.Join();//让消费线程借宿
    
                _ewh.Close();//释放event wait handle
    
            }
    
        }
    

    ○ EnqueueTask方法,让任务进入队列,每个进入队列的任务使用Set方法发出通知,产生任务的过程就是所谓的"生产"
    ○ Wokr方法,在没有task的时候,使用WaitOne方法一直等待;当任务出列,就执行任务,执行任务的过程就是所谓的"消费"
    ○ 构造函数创建、启动等待线程,让等待线程一直工作者(通过无限循环)

     

    客户端调用。

        class Program
    
        {
    
            static void Main(string[] args)
    
            {
    
                using (ProducerConsumerQueue q = new ProducerConsumerQueue())
    
                {
    
                    q.EnqueueTask("hello");
    
                    for (int i = 0; i < 3; i++)
    
                    {
    
                        q.EnqueueTask("报数" + i);
    
                    }
    
                    q.EnqueueTask("world");
    
                }
    
            }
    
        }

    44

     

    □ 手动控制线程的数量

     

    ■ 使用ManualResetEvent

    如果把AutoResetEvent比作地铁入口的十字转门,一次只能允许一个人进入;ManualResetEvent可看作公司门卫,上班时间到,打开门可以让多人进入。ManualResetEvent的Set方法就如同开门,任意多个线程可以进入,Reset方法如同关门,线程从此不能再进入。

     

    创建ManualResetEvent实例有2种方式:

    var manual1 = new ManualResetEvent (false);
    
    var manual2 = new EventWaitHandle (false, EventResetMode.ManualReset);

     

    以下是EventWaitHandle的一个简单应用:

        class Program
    
        {
    
            static EventWaitHandle handle = new ManualResetEvent(false);
    
            static void Main(string[] args)
    
            {
    
                handle.Set();
    
                new Thread(SaySth).Start("Hello");
    
                new Thread(SaySth).Start("World");
    
                Thread.Sleep(2000);
    
                handle.Reset();
    
                new Thread(SaySth).Start("Again");
    
            }
    
            static void SaySth(object data)
    
            {            
    
                handle.WaitOne();
    
                Console.WriteLine("我想说的是:" + data);
    
            }
    
        }
    

    45

    ○ Set方法,相当于开门,其后面的2个线程有效
    ○ Reset方法,相当于关门,其后面的1个线程无效

     

    ■ 使用CountdownEvent

    CountdownEvent也可以看作公司门卫,只不过,上班时间到,规定只允许若干个人进去。

        class Program
    
        {
    
            static CountdownEvent _countdown = new CountdownEvent(2);
    
            static void Main(string[] args)
    
            {
    
                new Thread(SaySth).Start("1");
    
                new Thread(SaySth).Start("2");           
    
            }
    
            static void SaySth(object o)
    
            {
    
                Thread.Sleep(1000);
    
                Console.WriteLine(o);
    
                _countdown.Signal();
    
            }
    
        }
    

    46

    ○ 在CountdownEvent的构造函数中设置允许的最大线程数
    ○ Signal方法表示计数一次


    总结:

    ○ 使用AutoResetEvent类,可以让一个线程等待另一个线程的通知,2个线程互相通知等待,一个线程等待队列中的多个任务通知
    ○ 使用ManualResetEvent类,手动控制任意多的线程数量
    ○ CountdownEvent类,手动控制固定数量的线程数量

     

     

    线程系列包括:

    线程系列01,前台线程,后台线程,线程同步

    线程系列02,多个线程同时处理一个耗时较长的任务以节省时间

    线程系列03,多线程共享数据,多线程不共享数据

    线程系列04,传递数据给线程,线程命名,线程异常处理,线程池

    线程系列05,手动结束线程

    线程系列06,通过CLR代码查看线程池及其线程

    线程系列07,使用lock语句块或Interlocked类型方法保证自增变量的数据同步

    线程系列08,实现线程锁的各种方式,使用lock,Montor,Mutex,Semaphore以及线程死锁

    线程系列09,线程的等待、通知,以及手动控制线程数量

    线程系列10,无需显式调用线程的情形

  • 相关阅读:
    【目标检测】RCNN算法详解
    自己搭建传统ocr识别项目学习
    015. asp.net实现简易聊天室
    014. asp.net实现记住密码的功能
    013. asp.net统计网站访问人数
    012. asp.net生成验证码图片(汉字示例/字母+数字)
    011. asp.net内置对象
    010. 使用.net框架提供的属性
    001. 使用ssh连接不上centos 6.5的解决方法及其解决中文乱码
    009. C#中的WebBrowser控件的属性、方法及操作演示代码(转)
  • 原文地址:https://www.cnblogs.com/darrenji/p/3991156.html
Copyright © 2020-2023  润新知