首页 > 其他分享 >【.NET】多线程:自动重置事件与手动重置事件的区别

【.NET】多线程:自动重置事件与手动重置事件的区别

时间:2023-11-11 21:22:19浏览次数:35  
标签:true 重置 线程 信号 QueueUserWorkItem NET 多线程 keyChars

在多线程编程中,如果每个线程的运行不是完全独立的。那么,一个线程执行到某个时刻需要知道其他线程发生了什么。嗯,这就是所谓线程同步。同步事件对象(XXXEvent)有两种行为:

1、等待。线程在此时会暂停运行,等待其他线程发出信号才继续(等你约);

2、发出信号。当前线程发出信号,其他正在等待线程收到信号后继续运行(我约你)。

从前,小明、小伟、小更、小红、小黄计划到野外去烤鱼吃。但他们只确定市郊东南方向的一片区域,并不能保证具体哪个地点适合烧烤。于是,他们商量好,大家同时从家里出发。小明离那里比较近,他先去考察一下;其他人到了东南郊后集合,等小明的消息。小明考察完毕,向大家群发消息说明选定的地点是F。最后大家继续前行,奔向F。

等待事件有好几个:

1、Mutex:互斥体。一次只能有一个线程获取到互斥体,其他线程只能等。占用互斥体的线程释放后,其他线程继续抢 Mutex。然后只有一个线程能抢到,其他线程继续等……

2、AutoResetEvent:自动事件,发出信号后立刻重置。

3、ManualResetEvent:手动事件,发出信号后不会立刻重置,得手动重置。

4、CountdownEvent:这个和上面两个差不多。但它会设定一个计数,线程发出信号时会减少计数。被阻止的线程要等到计数 <= 0 时才获得信号。

 

本次咱们讨论的重点是看看自动重置信号和手动重置信号之间有什么区别。

 先看看自动重置的。

internal class Program
{

    static AutoResetEvent theEvent = new(false);

    static void Main(string[] args)
    {
        // 启动三个线程
        ThreadPool.QueueUserWorkItem(DoWorking, "A");
        ThreadPool.QueueUserWorkItem(DoWorking, "B");
        ThreadPool.QueueUserWorkItem(DoWorking, "C");
        // 主线程监听键盘消息
        while(true)
        {
            var keyInfo = Console.ReadKey(true);
            // 看看是不是Y键
            if(keyInfo.Key == ConsoleKey.Y)
            {
                // 点亮信号
                theEvent.Set();
            }
            // 输出一行,方便判断一个循环
            Console.WriteLine("------------------------------");
        }
    }

    static void DoWorking(object? state)
    {
        while(true)
        {
            // 等待主线程的信号
            // 此线程会暂停
            theEvent.WaitOne();
            // 得到信号了,继续运行
            Console.WriteLine("{0}已收到通知", state);
        }
    }
}

这个例子创建了三个线程,这里我用的是线程池,把一个WaitCallback委托传给 QueueUserWorkItem 方法就可以在线程池中运行新线程。上面示例中绑定的方法是 DoWorking。

AutoResetEvent 类的构造函数传了一个 bool 值,它的作用是设置等待事件的初始状态:

1、如果为 true,表示事件初始状态为打开信号,这会使正在等的线程马上得到信号;

2、如果为 false,表示事件的初始状态为没有信号,正在等待的线程继续等。

按照咱们这个例子的实际情况,我们一开始应该让事件无状态,让后台的三个线程等待。主线程读取按键信息,如果按的是【Y】键,那么事件调用 Set 方法,打开信号。此时,等得花儿都谢了的三个线程会继续。我们运行一下,看看能否符合预期。

经测试,我们会发现:每次按【Y】后,三个线程中只有一个获得信号并继续,其他两个还在高速上堵车。 AutoResetEvent 的自动重置就是打开信号后又立马关闭,每次只让一个线程收到信号。所以,当咱们按一次【Y】键后,主线程发出了信号,又马上关闭。三个后台线程相互竞争,随机获得机会,结束等待并继续运行。

 

手动重置事件在打开信号后,信号会持续有效,直到调用 Reset 方法手动关闭信号。手动重置信号能让多个线程有足够的时间收到信号。

下面咱们把上面的示例改为使用 ManualResetEvent 类。

internal class Program
{
    static ManualResetEvent theEvent = new(false);

    static void Main(string[] args)
    {
        // 启动三个线程
        ThreadPool.QueueUserWorkItem(DoWorking, "A");
        ThreadPool.QueueUserWorkItem(DoWorking, "B");
        ThreadPool.QueueUserWorkItem(DoWorking, "C");
        // 主线程监听键盘消息
        while(true)
        {
            var keyInfo = Console.ReadKey(true);
            // 看看是不是Y键
            if(keyInfo.Key == ConsoleKey.Y)
            {
                // 点亮信号
                theEvent.Set();

                // 持续一段时间后关闭信号
                Thread.Sleep(3);
                theEvent.Reset();
            }
            // 输出一行,方便判断一个循环
            Console.WriteLine("------------------------------");
        }
    }

    static void DoWorking(object? state)
    {
        while(true)
        {
            // 等待主线程的信号
            // 此线程会暂停
            theEvent.WaitOne();
            // 得到信号了,继续运行
            Console.WriteLine("{0}已收到通知", state);
        }
    }
}

然后运行程序,这一次按下【Y】键后,三个线程都能收到信号通知了。

你会发现,有些线程重复了多次,那是因为 DoWorking 方法里面是个死循环。当信号持续打开期间,三个线程都有机会收到信号,甚至会重复收到。

上面的东东纯属演示,实际使用的话不会这样设计。最好的方法是建一个列表对象,主线程接收到的按键字符存放到一个列表中,然后,后台线程不断地从列表中取出元素来处理。这样设计程序会更流畅。

internal class Program
{
    #region 字段区域
    static Queue<char> keyChars = new();
    #endregion

    static void Main(string[] args)
    {
        // 启动三个线程
        ThreadPool.QueueUserWorkItem(DoSomething, "A");
        ThreadPool.QueueUserWorkItem(DoSomething, "B");
        ThreadPool.QueueUserWorkItem(DoSomething, "C");

        while(true)
        {
            // 读取键盘字符
            ConsoleKeyInfo info = Console.ReadKey(true);
            // 将字符放入队列
            keyChars.Enqueue(info.KeyChar);
        }
    }

    static void DoSomething(object? state)
    {
        while(true)
        {
            // 锁定
            Monitor.Enter(keyChars);
            if (keyChars.Count > 0)
            {
                // 取掉一个元素
                char c = keyChars.Dequeue();
                Console.WriteLine($"线程【{state}】获得字符:{c}");
            }
            // 解锁
            Monitor.Exit(keyChars);
        }
    }
}

这里我用泛型队列 Queue<T> 来存放键盘敲入的字符,DoSomething 方法将放入线程池中运行。在从队列中取出元素并处理时,一定要记得上锁。我用的是 Monitor 对象的静态方法来上锁和解锁,当然你可以用 lock 语句块。

lock(keyChars)
{
    ……
}

如果不上锁,线程间在抢占资源时会导致不一致的状态。当A线程访问 keyChars.Count 属性时得到 1,还是 > 0 的,但在取出最后一个元素前,偏偏B线程动作快把最后一个元素拿走了。当A线程执行到 keyChars.Dequeue() 一句时,keyChars 队列中已经没有元素了,会发生错误。

主线程在 Enqueue 时并不需要锁定,因为元素送入队列只有一个线程在做,没人跟他抢资源,可以不锁定。

运行程序后,可以按字母、数字等按键来测试。毕竟像【F3】、【Ctrl】等按键获取到的是空白 char。

这样就顺畅很多了。

 

标签:true,重置,线程,信号,QueueUserWorkItem,NET,多线程,keyChars
From: https://www.cnblogs.com/tcjiaan/p/17826114.html

相关文章

  • 无涯教程-批处理 - NET COMPUTER函数
    添加或删除连接到Windows域控制器的计算机。NETCOMPUTER-语法NETCOMPUTER\\computername{/ADD|/DEL}NETCOMPUTER-示例NETCOMPUTER\\dxbtest/ADD上面的命令会将名称为dxbtest的计算机添加到Windows域控制器所在的域中。参考链接https://www.learnfk.com/batch-......
  • 无涯教程-批处理 - NET CONFIG函数
    显示您当前的服务器或工作组设置。NETCONFIG-语法NETCONFIGNETCONFIG-示例NETCONFIG运行上面代码输出Thefollowingrunningservicescanbecontrolled:ServerWorkstationThecommandcompletedsuccessfully.参考链接https://www.learnfk.com/batch-......
  • SuperGlue: Learning Feature Matching with Graph Neural Networks论文笔记
    SuperGlue:LearningFeatureMatchingwithGraphNeuralNetworks源码:github.com/magicleap/SuperGluePretrainedNetwork背景:主要解决图像中点之间的对应关系。主要方法:上图为该方法的主要框架。模型大致分为两个部分:注意图神经网络和最优匹配层。其中第i个局部特征由di......
  • Performance Improvements in .NET 8 -- Native AOT & VM & GC & Mono
    原生AOT原生AOT在.NET7中发布。它使.NET程序在构建时被编译成一个完全由原生代码组成的自包含可执行文件或库:在执行时不需要JIT来编译任何东西,实际上,编译的程序中没有包含JIT。结果是一个可以有非常小的磁盘占用,小的内存占用,和非常快的启动时间的应用程序。在.NET7......
  • Performance Improvements in .NET 8 -- Native AOT & VM & GC & Mono
    原生AOT原生AOT在.NET7中发布。它使.NET程序在构建时被编译成一个完全由原生代码组成的自包含可执行文件或库:在执行时不需要JIT来编译任何东西,实际上,编译的程序中没有包含JIT。结果是一个可以有非常小的磁盘占用,小的内存占用,和非常快的启动时间的应用程序。在.NET7......
  • Redis6.0使用多线程是怎么回事?
    Redis不是说用单线程的吗?怎么6.0成了多线程的?Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。为什么命令执行为什么不采用多线......
  • 如何解决多线程下的共享对象问题?分布式系统又该如何应对?
    嗨,各位小米粉丝们!欢迎来到小米带你飞的微信公众号!今天我们要聊的话题可是程序员们都头疼的大问题哦——多线程情况下的对象共用问题,以及在分布式系统中的应对策略!小米要给大家详细解读一下,让你的技术面试不再被问倒!多线程中,如何解决对象共用问题?首先,我们得先了解多线程带来的挑战。......
  • windows自带工具netsh trace 抓包
     简单实例 管理员模式运行netshtracestartcapture=yesreport=disabled protocol=TCPipv4.address=192.168.0.40tracefile=d:\a.etl  停止抓包netshtracestop  -------------------------------------------------------------其它可选参数 report=e......
  • kubeadm部署的k8s证书过期问题 k8s问题排查:the existing bootstrap client certifica
     解决问题:估计跟移动有关,下面那个没解决问题,是因为在原有文件的基础上修改的吧?而这里直接是移走,重新生成了新的。不太清楚是不是这个原因。$cd/etc/kubernetes/pki/$mv{apiserver.crt,apiserver-etcd-client.key,apiserver-kubelet-client.crt,front-proxy-ca.crt,front......
  • Neural Networks投稿要求总结
    自用,NN投稿要求,相关的部分的中文版翻译,原文链接:https://www.sciencedirect.com/journal/neural-networks/publish/guide-for-authorsNeuralNetworks投稿要求介绍国际神经网络学会、欧洲神经网络学会和日本神经网络学会的官方期刊。论文类型Articles考虑原创的长篇文章时,应......