线程系列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("收到指示~~");
        }
    }

○ 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);
                }
            }
        }
    }

把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");
            }
        }
    }

□ 手动控制线程的数量

■ 使用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);
        }
    }


○ 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();
        }
    }


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

总结:

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

线程系列包括:

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

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

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

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

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

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

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

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

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

时间: 2024-10-18 04:48:58

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

Java线程与并发编程实践----等待通知(生产者消费者问题)线程

Java提供了一套API来支持线程之间的交互.在Object类中提供了一套等待通知的API  wait()     notify()     notifyAll()     此处要注意的是,绝不要在循环外面调用wait()方法.(单独开一片文章来讨论)     下面使用消费者与生产者问题来展示以上API的使用: package xiancheng; public class PC { public static void main(String[] args) { Shared s = new 

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

通常,我们会通过线程的构造函数先创建线程再使用线程.而实际上,.NET中有些类提供的方法,其内部就是使用多线程处理的.一些封装了多线程.异步处理方法的类都符合了"事件驱动异步模式(event-based asynchronous pattern)".以System.ComponentModel下的BackgroundWorker类来说,该类就符合这种模式. BackgroundWorker类属性:WorkerSupportsCancellation:设置为true表示允许取消Worke

内置锁(二)synchronized下的等待通知机制

一.等待/通知机制的简介 线程之间的协作: ??为了完成某个任务,线程之间需要进行协作,采取的方式:中断.互斥,以及互斥上面的线程的挂起.唤醒:如:生成者--消费者模式.或者某个动作完成,可以唤醒下一个线程.管道流已准备等等: 等待/通知机制: ? ?等待/通知机制 是线程之间的协作一种常用的方式之一,在显示锁Lock 和 内置锁synchronized都有对应的实现方式. 等待/通知机制 经典的使用方式,便是在生产者与消费者的模式中使用: 1.生产者负责生产商品,并送到仓库中存储: 2.消费者

二 Java利用等待/通知机制实现一个线程池

接着上一篇博客的 一Java线程的等待/通知模型 ,没有看过的建议先看一下.下面我们用等待通知机制来实现一个线程池 线程的任务就以打印一行文本来模拟耗时的任务.主要代码如下: 1  定义一个任务的接口. 1 /* 2 * 任务的接口 3 */ 4 public interface Task { 5 void doSomething(); 6 } 2  实现一个具体的任务. 1 /* 2 * 具体的任务 3 */ 4 public class PrintTask implements Task{

【线程系列三】线程的等待与唤醒机制

为了更高效的处理一些时间片短,任务量大的任务,我们可能会经常用到多线程.但是多线程的环境下,很容易出现线程并发问题,线程死锁就是很常见的一种并发问题.为了避免此类问题,我们会用到线程间的通信,而等待唤醒机制,就是线程间通信的一种形式. 等待唤醒机制用到的方法主要有: public final void wait() throws InterruptedException : 当前线程必须拥有此对象监视器.该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify 方法,或 notif

7种创建线程方式,你知道几种?线程系列Thread(一)

前言 最近特别忙,博客就此荒芜,博主秉着哪里不熟悉就开始学习哪里的精神一直在分享着,有着扎实的基础才能写出茁壮的代码,有可能实现的逻辑有多种,但是心中必须有要有底哪个更适合,用着更好,否则则说明我们对这方面还比较薄弱,这个时候就得好好补补了,这样才能加快提升自身能力的步伐,接下来的时间会着重讲解线程方面的知识.强势分割线. 话题乱入,一到跳槽季节想必我们很多人就开始刷面试题,这种情况下大部分都能解决问题,但是这样的结果则是导致有可能企业招到并非合适的人,当然作为面试官的那些人们也懒得再去自己出一

C#线程系列讲座(1):BeginInvoke和EndInvoke方法

  Normal 0 7.8 磅 0 2 false false false MicrosoftInternetExplorer4 /* Style Definitions */ table.MsoNormalTable { mso-style-parent:""; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman";

死磕 java线程系列之自己动手写一个线程池(续)

(手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的任务的,那么,我们自己能否实现呢?必须可以,今天我们就一起来实现带返回值任务的线程池. 前情回顾 首先,让我们先回顾一下上一章写的线程池: (1)它包含四个要素:核心线程数.最大线程数.任务队列.拒绝策略: (2)它具有执行无返回值任务的能力: (3)它无法处理有返回值的任务: (4)它无法处理任务

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

本篇体验:如何传递数据给线程,如何给线程命名,线程的异常处理,线程池.实在是太基础的部分. □ 传递数据给线程 ※ 使用Lambda表达式 class Program { static void Main(string[] args) { Thread t = new Thread(() => Say("hello", "world")); t.Start(); } static void Say(string msg, string msg1) { Cons