有什么不明白的地方,扫描右方二维码加我微信交流。
       

ManualResetEvent类是一个比较常用的线程间通信工具,可以手动阻塞线程,所以在线程同步方面非常有用。

主要使用3个方法:

  • WaitOne
  • Set
  • Reset

先引入一个概念,信号。因为官方解释中使用的是signaled,所以在这里翻译为信号。那么信号是什么,接着往下看。

WaitOne

调用WaitOne时会导致线程阻塞,但此时是否阻塞是有条件的,要看此时是否有信号。有信号就不阻塞,无信号就阻塞。那么是否有信号怎么判断?不用判断,这个我们不用关心,我们需要做的是控制什么时候有信号,什么时候无信号。

Set

调用Set方法,此时为有信号状态。

Reset

调用Reset方法,此时为无信号状态。

拿生产者和消费者都举例:

生产者线程:我又造出10个产品,快来消费吧!(调用Set接口,切换为有信号状态,通知消费者线程可以开始消费了)。

消费者线程:收到通知,消费这10个产品,消费完成后,又处于等待状态。(调用Reset接口,切换为无信号状态,再调用WaitOne接口,阻塞消费线程)

下面上代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ThreadTest
{
    class Program
    {
        static void Main(string[] args)
        {
            new ProductAndCostTester();
        }
    }

    /// <summary>
    /// 生产消费模型
    /// </summary>
    public class ProductAndCostTester
    {
        /// <summary>
        /// 生产线1线程
        /// </summary>
        private Thread _producterThread1;
        /// <summary>
        /// 生产线2线程
        /// </summary>
        private Thread _producterThread2;
        /// <summary>
        /// 消费线线程
        /// </summary>
        private Thread _costerThread;
        /// <summary>
        /// 产品列表
        /// </summary>
        private List<int> _goodList;
        /// <summary>
        /// ManualResetEvent实例
        /// </summary>
        private ManualResetEvent _mre;

        public ProductAndCostTester()
        {
            _goodList = new List<int>();

            _mre = new ManualResetEvent(false);//false初始化状态为无信号,将使WaitOne阻塞

            _producterThread1 = new Thread(Product1);
            _producterThread1.Name = "Productor1";
            _producterThread1.Start();

            _producterThread2 = new Thread(Product2);
            _producterThread2.Name = "Productor2";
            _producterThread2.Start();

            _costerThread = new Thread(Cost);
            _costerThread.Name = "Costor";
            _costerThread.Start();
        }

        /// <summary>
        /// 生产线1
        /// </summary>
        void Product1()
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + DateTime.Now.ToString("HH:mm:ss"));
                for (int i = 0; i < 3; i++)
                {
                    _goodList.Add(1);
                }
                _mre.Set();//表示有信号了,通知WaitOne不再阻塞

                Thread.Sleep(8000);
            }
        }

        /// <summary>
        /// 生产线2
        /// </summary>
        void Product2()
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + DateTime.Now.ToString("HH:mm:ss"));
                for (int i = 0; i < 6; i++)
                {
                    _goodList.Add(1);
                }
                _mre.Set();//表示有信号了,通知WaitOne不再阻塞

                Thread.Sleep(10000);
            }
        }

        /// <summary>
        /// 消费线
        /// </summary>
        void Cost()
        {
            while (true)
            {
                if (_goodList.Count > 0)
                {
                    Console.WriteLine("Cost " + _goodList.Count + " at " + DateTime.Now.ToString("HH:mm:ss"));
                    _goodList.Clear();
                    _mre.Reset();//重置为无信号了,使WaitOne可以再次阻塞
                }
                else
                {
                    Console.WriteLine("No cost at " + DateTime.Now.ToString("HH:mm:ss"));
                    _mre.WaitOne();//如果没有可消费的产品,即无信号,则会阻塞
                }
            }
        }
    }
}

以上代码可以直接拷贝到脚本中执行,亲测。

 

ManualResetEvent类似的还有AutoResetEventManualResetEventSlim等等。AutoResetEventManualResetEvent都集成自EventWaitHandle,二者原理相同,用法不同,AutoResetEvent直接调用WaitOne阻塞,无需调用Reset。下面着重说下ManualResetEventSlimManualResetEvent的区别。

ManualResetEvent:调用ResetWaitOne后,会阻塞线程,阻塞的实现是release线程,线程此时不消耗任何资源,适合长时间等待的线程阻塞。

ManualResetEventSlim:调用ResetWait后,会阻塞线程,阻塞的实现是让线程跑一个for循环,for循环结束后再使用lock,lock住线程,此时线程不会释放,但会占用一定的CPU,适合短时间的线程阻塞。

如何理解长时间,短时间?看下ManualResetEventSlim类里Wait的实现:代码太长了,点击链接自行查看

SpinCount可以在创建ManualResetEventSlim实例时设置,默认为10,最大为2047(代码中没有明确说明,自测出结果),在for循环中调用了Thread.Yield()Thread.SpinWait(4 << i),还有Thread.Sleep(0)Thread.Sleep(1)

 

方法耗时:

(机器配置不同,耗时可能不同,但效果是一样的,我的配置为2 GHz 四核Intel Core i5,16 GB,512SSD)

Thread.Yeild:0.1ms/个;

Thread.SpinWait(3960):0.2242ms(SpinCount超过10时,SpinWait方法共消耗时间);

Thread.Sleep(0):0.14ms;

Thread.Sleep(1):1.38ms;

 

Thread.Yeild

该方法是在 .Net 4.0 中推出的新方法,它对应的底层方法是 SwitchToThread。Yield 的中文翻译为 “放弃”,这里意思是主动放弃当前线程的时间片,并让操作系统调度其它就绪态的线程使用一个时间片。但是如果调用 Yield,只是把当前线程放入到就绪队列中,而不是阻塞队列。如果没有找到其它就绪态的线程,则当前线程继续运行。
优势:比 Thread.Sleep(0) 速度要快,可以让低于当前优先级的线程得以运行。可以通过返回值判断是否成功调度了其它线程。
劣势:只能调度同一个处理器的线程,不能调度其它处理器的线程。当没有其它就绪的线程,会一直占用 CPU时间片,造成 CPU 100%占用率。

 

Thread.SpinWait(int n)

执行等待时,会进行自旋。所谓自旋就是在CPU运转的周期内,如果条件满足了,就不会再进入内核等待(即暂停该线程,等待一段时间后,再继续运行该线程),如果条件不满足,才进入内核等待。这样一来,SpinWait会比Thread.Sleep多运行一次的CPU周期,再进入等待。因为CPU周期是很短的(现在一般的电脑都有2.1GHZ以上),所以这个等待对时间影响不大,却可以提升很大的性能。

 

Thread.Sleep(0)

Sleep 的意思是告诉操作系统自己要休息 n 毫秒,这段时间就让给另一个就绪的线程吧。当 n=0 的时候,意思是要放弃自己剩下的时间片,但是仍然是就绪状态,其实意思和 Yield 有点类似。但是 Sleep(0) 只允许那些优先级相等或更高的线程使用当前的CPU,其它线程只能等着挨饿了。如果没有合适的线程,那当前线程会重新使用 CPU时间片。
优势:相比 Yield,可以调度任何处理器的线程使用时间片。
劣势:只能调度优先级相等或更高的线程,意味着优先级低的线程很难获得时间片,很可能永远都调用不到。当没有符合条件的线程,会一直占用 CPU时间片,造成 CPU100%占用率。

 

Thread.Sleep(1)

该方法使用 1 作为参数,这会强制当前线程放弃剩下的时间片,并休息 1 毫秒(因为不是实时操作系统,时间无法保证精确,一般可能会滞后几毫秒或一个时间片)。但因此的好处是,所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。
优势:可以调度任何处理器的线程使用时间片。无论有没有符合的线程,都会放弃 CPU时间,因此 CPU占用率较低。
劣势:相比 Thread.Sleep(0),因为至少会休息一定时间,所以速度要更慢。

 

SpinCount,0-9,调用Thread.Yield()Thread.SpinWait(4 << i);循环下来,4<<1+4<<2+4<<3+4<<4+4<<6+4<<7+4<<8+4<<9=3960,一共SpinWait了8次,共3960,Thread.SpinWait是多长时间?我在电脑上测试,时间大概为0.0002242s,即0.2242ms,外加2个Thread.Yield(),共耗时:0.2242+2*0.1=0.4242ms。

SpinCount,10-19,调用8Thread.Yield()和2个Thread.Sleep(0),共耗时:8*0.1+2*0.14=1.04ms。

SpinCount,20-2047,调用Thread.Yield()Thread.Sleep(0)Thread.Sleep(1);每20个数里,调用1次Thread.Sleep(1),3次Thread.Sleep(0),16次Thread.Yield(),粗略算下101个循环,共耗时:(1.38+3*0.14+16*0.1)*101=340ms

如果SpinCount为2047,则总耗时为:总耗时:0.4242+1.04+340=341.4642ms。

 

这期间,进行了大量的Thread.Yield()Thread.Sleep(0)Thread.Sleep(1)3个方法调用,不可能没有消耗,所以当时间超过500ms甚至超过300ms时,我觉得使用ManualResetEventSlim就不再合适了。

 

由此得出结论,短时间就是很短的时间,可以忽略的时间,但凡等待超过1s,那都是很长的时间了。

发表评论

邮箱地址不会被公开。 必填项已用*标注