线程同步

一、为什么使用线程同步?

如今的应用程序越来越复杂,我们常常需要多线程技术来提高我们应用程序的响应速度,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。每个线程都由自己的线程ID,当前指令指针(PC),寄存器集合和堆栈组成,但代码区是共享的,即不同的线程可以执行同样的函数。所以在并发环境中,多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,会造成共享数据损坏,所以就要使线程同步。(如果多个线程同时对共享数据只进行只读访问是不需要进行同步的)

二、线程同步带来的问题?  

在并发的环境里,“线程同步锁”可以保护共享数据,但是也会存在一些问题:

  • 实现比较繁琐,而且容易错漏。你必须标识出可能由多个线程访问的所有共享数据。然后,必须为其获取和释放一个线程同步琐,并且保证已经正确为所有共享资源添加了锁定代码。

  • 由于临界区无法并发运行,进入临界区就需要等待,加锁带来效率的降低。

  • 在复杂的情况下,很容易造成死锁,并发实体之间无止境的互相等待。

  • 优先级倒置造成实时系统不能正常工作。优先级低的进程拿到高优先级进程需要的锁,结果是高/低优先级的进程都无法运行,中等优先级的进程可能在狂跑。

  • 当线程池中一个线程被阻塞时,可能造成线程池根据CPU使用情况误判创建更多的线程以便执行其他任务,然而新创建的线程也可能因请求的共享资源而被阻塞,恶性循环,徒增线程上下文切换的次数,并且降低了程序的伸缩性。(这一点很重要)

三、NET下线程同步的方法?

线程同步即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,目前实现线程同步的方法有很多,其中包括临界区、互斥量、事件、信号量四种方式。  
  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(临界区可以认为是操作共享资源的一段代码

  • 互斥量:为协调共同对一个共享资源的单独访问而设计的。

  • 信号量:为控制一个具有有限数量用户资源而设计。

  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始  

四、临界区

适用范围:它只能同步一个进程中的线程,不能跨进程同步。一般用它来做单个进程内的代码快同步,效率比较高。通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。实现临界区的方式包括Lock、Monitor类、ReaderWriterLock类以及标志量

1、Lock

(1)lock的使用形式:lock(expression) embedded-statement, 即lock   (   表达式   )   嵌入语句。注意:lock 语句的表达式必须表示一个引用类型的值。永远不会为 lock 语句中的表达式执行隐式装箱转换,因此,如果该表达式表示的是一个值类型的值,则会导致一个编译时错误。
(2)lock的实质。lock (x) 等价于以下代码(其中x是引用类型)        
        system.threading.monitor.enter(x);
        try {
             ...
          }
        finally 
          {           system.threading.monitor.exit(x);           }

(3)lock什么对象

  • lock引用类型
  • 某些系统类提供专门用于锁定的成员。例如,array 类型提供 syncroot。许多集合类型也提供 syncroot。
  • 自定义类推荐用私有的只读静态对象,比如:
private static readonly object obj = new object();

  为什么要设置成只读的呢?这时因为如果在lock代码段中改变obj的值,其它线程就畅通无阻了,因为互斥锁的对象变了,object.referenceequals必然返回false。(推荐的方式

 
(4)为什么只能lock引用类型?
  • 为什么不能lock值类型,比如lock(1)呢?lock本质上monitor.enter,monitor.enter会使值类型装箱,每次lock的是装箱后的对象。lock其实是类似编译器的语法糖,因此编译器直接限制住不能lock值类型。

  • 退一万步说,就算能编译器允许你lock(1),但是object.referenceequals(1,1)始终返回false(因为每次装箱后都是不同对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样能够访问里面的代码,达不到同步的效果。同理lock((object)1)也不行。

  • 那么lock("xxx")字符串呢?msdn上的原话是:锁定字符串尤其危险,因为字符串被公共语言运行库 (clr)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。

  • 通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。

  • lock(typeof(class))与锁定字符串一样,范围太广了。

(5)示例

/*
该实例是一个线程中lock用法的经典实例,使得到的balance不会为负数
同时初始化十个线程,启动十个,但由于加锁,能够启动调用WithDraw方法的可能只能是其中几个
*/
using System;

namespace ThreadTest29
{
    class Account
    {
        private Object thisLock = new object();//设置锁对象
        int balance;
        Random r = new Random();

        public Account(int initial)
        {
            balance = initial;
        }

        int WithDraw(int amount)
        {
            if (balance < 0)
            {
                throw new Exception("负的Balance.");
            }
            //确保只有一个线程使用资源,一个进入临界状态,使用对象互斥锁,10个启动了的线程不能全部执行该方法
            lock (thisLock)
            {
                if (balance >= amount)
                {
                    Console.WriteLine("----------------------------:" + System.Threading.Thread.CurrentThread.Name + "---------------");

                    Console.WriteLine("调用Withdrawal之前的Balance:" + balance);
                    Console.WriteLine("把Amount输入 Withdrawal     :-" + amount);
                    //如果没有加对象互斥锁,则可能10个线程都执行下面的减法,加减法所耗时间片段非常小,可能多个线程同时执行,出现负数。
                    balance = balance - amount;
                    Console.WriteLine("调用Withdrawal之后的Balance :" + balance);
                    return amount;
                }
                else
                {
                    //最终结果
                    return 0;
                }
            }
        }
        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {
                //生成balance的被减数amount的随机数
                WithDraw(r.Next(1, 100));
            }
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            //初始化10个线程
            System.Threading.Thread[] threads = new System.Threading.Thread[10];
            //把balance初始化设定为1000
            Account acc = new Account(1000);
            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(acc.DoTransactions));
                threads[i] = t;
                threads[i].Name = "Thread" + i.ToString();
            }
            for (int i = 0; i < 10; i++)
            {
                threads[i].Start();
            }
            Console.ReadKey();
        }
    }
}

2、Monitor类

这个算是实现锁机制的纯正类,在锁定的临界区中只允许让一个线程访问,其他线程排队等待。主要整理为2组方法。

(1)Monitor.Enter和Monitor.Exit      

  微软很照护我们,给了我们语法糖Lock,对的,语言糖确实减少了我们不必要的劳动并且让代码更可观,但是如果我们要精细的     控制,则必须使用原生类,这里要注意一个问题就是“锁住什么”的问题,一般情况下我们锁住的都是静态对象,我们知道静态对象属于类级别,当有很多线程共同访问的时候,那个静态对象对多个线程来说是一个,不像实例字段会被认为是多个。Monitor 锁定对象是引用类型,而非值类型,该对象用来定义锁的范围,与lock一样,毕竟lock是monitor的语法糖。

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(Run);

                t.Start();
            }
        }

        //资源
        static object obj = new object();

        static int count = 0;

        static void Run()
        {
            Thread.Sleep(10);

            //进入临界区
            Monitor.Enter(obj);

            Console.WriteLine("当前数字:{0}", ++count);

            //退出临界区
            Monitor.Exit(obj);
        }
    }

(2)Monitor.Wait和Monitor.Pulse  

首先这两个方法是成对出现,通常使用在Enter,Exit之间。 Wait: 暂时的释放资源锁,然后该线程进入”等待队列“中,那么自然别的线程就能获取到资源锁。 Pulse:  唤醒“等待队列”中的线程,那么当时被Wait的线程就重新获取到了锁。 这里我们是否注意到了两点:①   可能A线程进入到临界区后,需要B线程做一些初始化操作,然后A线程继续干剩下的事情。②   用上面的两个方法,我们可以实现线程间的彼此通信。

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

namespace Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            LockObj obj = new LockObj();

            //注意,这里使用的是同一个资源对象obj
            Jack jack = new Jack(obj);
            John john = new John(obj);

            Thread t1 = new Thread(new ThreadStart(jack.Run));
            Thread t2 = new Thread(new ThreadStart(john.Run));

            t1.Start();
            t1.Name = "Jack";

            t2.Start();
            t2.Name = "John";

            Console.ReadLine();
        }
    }

    //锁定对象
    public class LockObj { }

    public class Jack
    {
        private LockObj obj;

        public Jack(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:我已进入茅厕。", Thread.CurrentThread.Name);

            Console.WriteLine("{0}:擦,太臭了,我还是撤!", Thread.CurrentThread.Name);

            //暂时的释放锁资源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:兄弟说的对,我还是进去吧。", Thread.CurrentThread.Name);

            //唤醒等待队列中的线程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }

    public class John
    {
        private LockObj obj;

        public John(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:直奔茅厕,兄弟,你还是进来吧,小心憋坏了!",
                               Thread.CurrentThread.Name);

            //唤醒等待队列中的线程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:哗啦啦....", Thread.CurrentThread.Name);

            //暂时的释放锁资源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }
}

3、ReaderWriteLock类

先前也知道,Monitor实现的是在读写两种情况的临界区中只可以让一个线程访问,那么如果业务中存在”读取密集型“操作,就好比数据库一样,读取的操作永远比写入的操作多。针对这种情况,我们使用Monitor的话很吃亏,不过没关系,ReadWriterLock就很牛X,因为实现了”写入串行“,”读取并行“。ReaderWriteLock中主要用3组方法:

(1)AcquireWriterLock: 获取写入锁。

         ReleaseWriterLock:释放写入锁。

(2) AcquireReaderLock: 获取读锁。

           ReleaseReaderLock:释放读锁。

(3)UpgradeToWriterLock:将读锁转为写锁。

         DowngradeFromWriterLock:将写锁还原为读锁。

下面就实现一个写操作,三个读操作,要知道这三个读操作是并发的。

namespace Test
{
    class Program
    {
        static List<int> list = new List<int>();

        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)
        {
            Thread t1 = new Thread(AutoAddFunc);

            Thread t2 = new Thread(AutoReadFunc);

            t1.Start();

            t2.Start();

            Console.Read();
        }

        /// <summary>
/// 模拟3s插入一次
/// </summary>
/// <param name="num"></param>
        public static void AutoAddFunc()
        {
            //3000ms插入一次
            Timer timer1 = new Timer(new TimerCallback(Add), null, 0, 3000);
        }

        public static void AutoReadFunc()
        {
            //1000ms自动读取一次
            Timer timer1 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer2 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer3 = new Timer(new TimerCallback(Read), null, 0, 1000);
        }

        public static void Add(object obj)
        {
            var num = new Random().Next(0, 1000);

            //写锁
            rw.AcquireWriterLock(TimeSpan.FromSeconds(30));

            list.Add(num);

            Console.WriteLine("我是线程{0},我插入的数据是{1}。", Thread.CurrentThread.ManagedThreadId, num);

            //释放锁
            rw.ReleaseWriterLock();
        }

        public static void Read(object obj)
        {
            //读锁
            rw.AcquireReaderLock(TimeSpan.FromSeconds(30));

            Console.WriteLine("我是线程{0},我读取的集合为:{1}",
                              Thread.CurrentThread.ManagedThreadId, string.Join(",", list));
            //释放锁
            rw.ReleaseReaderLock();
        }
    }
}

4、标志量

 顾名思义,标志量就是声明一个布尔型变量,用来标示某些方法的执行状态。

五、互斥量 

考虑到互斥量、信号量、事件的出身,就免不了介绍WaitHandle类。CSDN对它的定义如下:封装等待对共享资源的独占访问的操作系统特定的对象。来看下面的继承层次结构图。从图中可以看出事件(EventWaitHandle )、互斥量(Mutex)、信号量(Semaphore)都是继承自System.Threading.WaitHandle。WaitHandle类的作用:可以调用它的方法,来等待若干个信号发生,即可以调节多个线程之间的同步。 

互斥量跟临界区很相似,只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。适用范围:可以跨进程同步,还可以用来保证程序只有一个实例运行(创建命名互斥量),也可以用来做线程间的同步 

1、互斥量metux

Metux中提供了WatiOne和ReleaseMutex来确保只有一个线程来访问共享资源,是不是跟Monitor很类似。先介绍一下Menux的特点:

(1)当给Mutex取名的时候能够实现进程同步,不取名实现线程同步,详细细节参考MSDN:

(2)Mutex封装了win32的同步机制,而Monitor是由framework封装,所以在线程同步角度来说,Monitor更加短小精悍,优于Mutex,要是实现进程 同步,Monitor也干不了,所以Mutex是首选。其实使用Win32同步机制的还有:事件与信号量

2、示例一:线程间的同步:注意:没有给Mutex起名字 

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 20; i++)
            {
                Thread t = new Thread(Run);

                t.Start();
            }

            Console.Read();
        }

        static int count = 0;

        static Mutex mutex = new Mutex();

        static void Run()
        {
            Thread.Sleep(100);

            mutex.WaitOne();

            Console.WriteLine("当前数字:{0}", ++count);

            mutex.ReleaseMutex();
        }
    }

3、实例二:进程间的示例:给Mutex起了名字 

class Program
    {
        static void Main(string[] args)
        {
            Thread t = new Thread(Run);

            t.Start();

            Console.Read();
        }

        static Mutex mutex = new Mutex(false, "cnblogs");

        static void Run()
        {
            mutex.WaitOne();

            Console.WriteLine("当前时间:{0}我是线程:{1},我已经进去临界区", DateTime.Now, Thread.CurrentThread.GetHashCode());

            //10s
            Thread.Sleep(10000);

            Console.WriteLine("
当前时间:{0}我是线程:{1},我准备退出临界区", DateTime.Now, Thread.CurrentThread.GetHashCode());

            mutex.ReleaseMutex();
        }
    }

六、信号量

1、什么是信号量?

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。信号量有一个使用计数器,这个使用计数器,是信号量的最大资源计数和当前资源计数的差值。

2、信号量的使用原理

a、如果当前资源计数大于0,那么信号量处于触发状态。 

b、如果当前资源计数等于0,那么信号量处于未触发状态。 

c、系统绝对不会让当前资源计数变为负数。 

d、当前资源计数绝对不会大于最大最大资源计数

3、示例一:线程间的sempahore同步

如果出现 WaitOne 就一个要有对应的Release 即获取信号量后,一定要释放。如:6个进程需要同时使用打印机,而电脑上只有四台打印机,则打印机是被保护的资源,信号量为4。则需要用semaphore来同步。 

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

namespace SemaphoreClass
{
    class Program
    {
        static void Main(string[] args)
        {
            int sourceCount = 4;//打印机数量            
            int threadCount = 6;//需要打印的线程数目
            
            // maximumCount-initialCount  之间的差值为已经锁定的 semaphore的数量(即信号量使用计数器)  此实例中已经指定占用了0个信号量
            //Semaphore的第三个参数为信号量的名称,如果设定了名称,则可用于进程间的同步,如果没有设置名称则只能用于进程内的线程同步
            Semaphore sempaphore = new Semaphore(sourceCount, sourceCount, "sempaphore");

            Thread[] threads = new Thread[threadCount];
            for (int i = 0; i < threadCount; i++)
            {
                threads[i] = new Thread(ThreadMain);
                threads[i].Start(sempaphore);
            }
            for (int i = 0; i < threadCount; i++)
            {
                threads[i].Join();
            }
            Console.WriteLine("All threads finished!");
            Console.ReadKey();
        }

        /// <summary>
        /// 线程执行的方法
        /// </summary>
        /// <param name="o"></param>
        static void ThreadMain(object o)
        {
            Semaphore semaphore = o as Semaphore;
            Trace.Assert(semaphore != null, "o must be a semphore type");

            bool isCompleted = false;
            while (!isCompleted)
            {
                //锁定信号量,如果锁定计数已经达到最高计数限制,则等待600毫秒。如果在600毫秒后未能获得锁定,则返回false。
                if (semaphore.WaitOne(600, false))
                {
                    try
                    {
                        Console.WriteLine("Thread {0} locks the semaphore ", Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(2000);
                    }
                    finally
                    {
                        //解除资源的锁定。参数为退出信号量的次数。占用一个信号量故退出一个。
                        semaphore.Release(1);
                        Console.WriteLine("Thread {0} release the semaphore", Thread.CurrentThread.ManagedThreadId);
                        isCompleted = true;
                    }
                }
                else
                {
                    Console.WriteLine("Timeot for thread {0}; wait again", Thread.CurrentThread.ManagedThreadId);
                }
            }
        }
    }
}

4、示例二:进程间的sempahore同步

下面的例子将使用信号量来同步进程,一个应用程序可以有二个实例运行(如果只允许有一个实例来运行,最优之选是mutex,其次才是信号量)。虽然这个例子不太实用,但完全可以说明semaphore的特性。注意:生成解决方案后运行三次生成EXE,就会看到结果。信号量的进程同步和信号量的应用程序的名称无关。只要使用了同样名称的信号量,他们之前就存在了一种协约。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace SemaphoreProcess
{
    class Program
    {
        static void Main(string[] args)
        {
            //定义可同步运行的可用实例数
            int CreateNew = 2;
 
            //定义可同步运行的最大实例数
            int MaxCreateNew = 5;
 
            // maximumCount-initialCount  之间的差值为已经锁定的 semaphore的数量  此实例中已经指定占用了3个信号量 计算方式为(MaxCreateNew-CreateNew)
            //Semaphore的第三个参数为信号量的名称,如果设定了名称,则可用于进程间的同步,如果没有设置名称则只能用于进程内的线程同步
            System.Threading.Semaphore sempaphore = new System.Threading.Semaphore(CreateNew, MaxCreateNew, "sempaphoreProcess");
 
            if (sempaphore.WaitOne(100, false))
            {
                Console.WriteLine("系统正在运行……");
                Console.ReadKey();
            }
            else
            {
                MessageBox.Show("当前已经有 " + CreateNew + " 个实例在运行,系统将退出!", "您好", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
 
        }
    }
}

七、事件

继续介绍WaitHandler类及其子类ManualResetEvent,AutoResetEvent的用法。.NET中线程同步的方式多的让人看了眼花缭乱,究竟该怎么去理解呢?其实,我们抛开.NET环境看线程同步,无非是执行两种操作:一是互斥/加锁,目的是保证临界区代码操作的“原子性”;另一种是信号灯操作,目的是保证多个线程按照一定顺序执行,如生产者线程要先于消费者线程执行。.NET中线程同步的类无非是对这两种方式的封装,目的归根结底都可以归结为实现互斥/加锁或者是信号灯这两种方式,只是它们的适用场合有所不同。下面我们根据类的层次结构了解WaitHandler及其子类。

1、WaitHandler

我们已经知道WaitHandle是Mutex,Semaphore,EventWaitHandler,AutoResetEvent,ManualResetEvent共同的祖先,它封装Win32同步句柄内核对象,也就是说是这些内核对象的托管版本。线程可以通过调用WaitHandler实例的方法WaitOne在单个等待句柄上阻止。此外,WaitHandler类重载了静态方法,以等待所有指定的等待句柄都已收集到信号WaitAll,或者等待某一指定的等待句柄收集到信号WaitAny。这些方法都提供了放弃等待的超时间隔、在进入等待之前退出同步上下文的机会,并允许其它线程使用同步上下文。WaitHandler是C#中的抽象类,不能实例化。

System.Threading.WaitHandle.WaitOne 使线程一直等待,直到单个事件变为终止状态;

System.Threading.WaitHandle.WaitAny 阻止线程,直到一个或多个指示的事件变为终止状态;

System.Threading.WaitHandle.WaitAll 阻止线程,直到所有指示的事件都变为终止状态。

2、ManualResetEvent和AutoResetEvent

区别:ManualResetEvent和AutoResetEvent都继承自EventWaitHandler,它们的唯一区别就在于父类EventWaitHandler的构造函数参数EventResetMode不同,这样我们只要弄清了参数EventResetMode值不同时,EventWaitHandler类控制线程同步的行为有什么不同,两个子类也就清楚了。

    1)AutoResetEvent.WaitOne()每次只允许一个线程进入,当某个线程得到信号后,AutoResetEvent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只有继续等待,也就是说AutoResetEvent一次只唤醒一个线程;
      2)ManualResetEvent则可以唤醒多个线程,因为当某个线程调用了ManualResetEvent.Set()方法后,其他调用WaitOne的线程获得信号得以继续执行,而ManualResetEvent不会自动将信号置为不发送。
      3)也就是说,除非手工调用了ManualResetEvent.Reset()方法,则ManualResetEvent将一直保持有信号状态,ManualResetEvent也就可以同时唤醒多个线程继续执行。

共同点:

  1)Set方法将事件状态设置为终止状态,允许一个或多个等待线程继续;Reset方法将事件状态设置为非终止状态,导致线程阻止;WaitOne阻止当前线程,直到当前线程的WaitHandler收到事件信号。
      2)可以通过构造函数的参数值来决定其初始状态,若为true则事件为终止状态从而使线程为非阻塞状态,为false则线程为阻塞状态。
      3)如果某个线程调用WaitOne方法,则当事件状态为终止状态时,该线程会得到信号,继续向下执行

3、代码示例

上边都是一些理论知识,主要是帮助掌握知识脉络,我们不仅会用,也得知道为什么这样用。假设有这样的一个场景,主线程开了一个子线程,让子线程等着,等主线程完成了某件事情时再通知子线程去往下执行,这里关键就在于这个怎么让子线程等着,主线程怎通知子线程,一般情况下我们不难想到用一个公共变量,于是咱们就有了下面的代码:

(1)设置公共变量实现功能。

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

namespace AutoResetEventTest
{
    class Class1
    {
        static bool flag = true;//设置公共变量

        static void DoWork()
        {
            Console.WriteLine("worker thread started, now waiting on event...");
            while (flag)
            {

            }
            Console.WriteLine("worker thread reactivated, now exiting...");
        }

        static void Main()
        {
            Console.WriteLine("main thread starting worker thread...");
            Thread t = new Thread(DoWork);
            t.Start();

            Console.WriteLine("main thrad sleeping for 1 second...");
            Thread.Sleep(1000);

            Console.WriteLine("main thread signaling worker thread...");
            flag = false;
            Console.ReadKey();
        }
    }
}
View Code

(2)AutoResetEvent实现功能

设置公共变量的方式虽然达到了目的,但是看着这代码就纠结,下面该是我们的主角上场了,AutoResetEvent 和 ManualResetEvent,这里以AutoResetEvent为例,其实很多官方的说法太过于抽象,这里通俗地讲,可以认为AutoResetEvent就是一个公共的变量(尽管它是一个事件),创建的时候可以设置为false,然后在要等待的线程使用它的WaitOne方法,那么线程就一直会处于等待状态,只有这个AutoResetEvent被别的线程使用了Set方法,也就是要发通知的线程使用了它的Set方法,那么等待的线程就会往下执行了,Set就是发信号,WaitOne是等待信号,只有发了信号,等待的才会执行。如果不发的话,WaitOne后面的程序就永远不会执行。好下面看用AutoResetEvent改造上面的程序:

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

namespace AutoResetEventTest
{
    class Class2
    {
        static AutoResetEvent mEvent = new AutoResetEvent(false);
        //static ManualResetEvent mEvent = new ManualResetEvent(false);  

        static void DoWork()
        {
            Console.WriteLine("worker thread started, now waiting on event...");
            mEvent.WaitOne();
            Console.WriteLine("worker thread reactivated, now exiting...");
        }

        static void Main()
        {
            Console.WriteLine("main thread starting worker thread...");
            Thread t = new Thread(DoWork);
            t.Start();

            Console.WriteLine("main thrad sleeping for 1 second...");
            Thread.Sleep(1000);

            Console.WriteLine("main thread signaling worker thread...");
            mEvent.Set();
            Console.ReadKey();
        }
    }
}
View Code

(3)使用AutoResetEvent使代码好多了,这里其实你还会看到,把上面的AutoResetEvent换成ManualResetEvent也是没有问题的,那么它两之间的区别是什么呢?AutoResetEvent.WaitOne()每次只允许一个线程进入,当某个线程得到信号后,AutoResetEvent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只有继续等待,也就是说AutoResetEvent一次只唤醒一个线程;ManualResetEvent则可以唤醒多个线程,因为当某个线程调用了ManualResetEvent.Set()方法后,其他调用WaitOne的线程获得信号得以继续执行,而ManualResetEvent不会自动将信号置为不发送,也就是说,除非手工调用了ManualResetEvent.Reset()方法,则ManualResetEvent将一直保持有信号状态,ManualResetEvent也就可以同时唤醒多个线程继续执行。

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

namespace AutoResetEventTest
{
    class Class3
    {
        static AutoResetEvent mEvent = new AutoResetEvent(false);
        //static ManualResetEvent mEvent = new ManualResetEvent(false);

        static void DoWork()
        {
            Console.WriteLine("   worker thread started, now waiting on event...");
            for (int i = 0; i < 3; i++)
            {
                mEvent.WaitOne();
                //mEvent.Reset();
                Console.WriteLine("   worker thread reactivated, now exiting...");
            }
        }

        static void Main()
        {
            Console.WriteLine("main thread starting worker thread...");
            Thread t = new Thread(DoWork);
            t.Start();

            for (int i = 0; i < 3; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine("main thread signaling worker thread...");
                mEvent.Set();
            }
        }
    }
}
View Code

(4)对于(3)中实例,如果你想仅仅把AutoResetEvent换成ManualResetEvent的话,你发现输出就会乱套了,为什么呢?

假如有autoevent.WaitOne()和manualevent.WaitOne(),当线程得到信号后都得以继续执行。差别就在调用后,autoevent.WaitOne()每次只允许一个线程进入,当某个线程得到信号(也就是有其他线程调用了autoevent.Set()方法后)后,autoevent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只有继续等待,也就是说,autoevent一次只唤醒一个线程。而manualevent则可以唤醒多个线程,当某个线程调用了set方法后,其他调用waitone的线程获得信号得以继续执行,而manualevent不会自动将信号置为不发送,也就是说,除非手工调用了manualevent.Reset()方法,否则manualevent将一直保持有信号状态,manualevent也就可以同时唤醒多个线程继续执行。在上面代码中,如果将AutoResetEvent换成ManualResetEvent的话,只要要在waitone后面做下reset,就会达到同样的效果。

4、说一个关于AutoResetEvent和ManualResetEvent的笑话 

示例场景:张三、李四两个好朋友去餐馆吃饭,两个人点了一份宫爆鸡丁,宫爆鸡丁做好需要一段时间,张三、李四不愿傻等,都专心致志的玩起了手机游戏,心想宫爆鸡丁做好了,服务员肯定会叫我们的。服务员上菜之后,张三李四开始享用美味的饭菜,饭菜吃光了,他们再叫服务员过来买单。我们可以从这个场景中抽象出来三个线程,张三线程、李四线程和服务员线程,他们之间需要同步:服务员上菜—>张三、李四开始享用宫爆鸡丁—>吃好后叫服务员过来买单。这个同步用什么呢? ManualResetEvent还是AutoResetEvent?通过上面的分析不难看出,我们应该用ManualResetEvent进行同步。   

      编译后查看运行结果,符合我们的预期,控制台输出为:
      服务员:厨师在做菜呢,两位稍等...
      张三:等着上菜无聊先玩会手机游戏
      李四:等着上菜无聊先玩会手机游戏
      张三:等着上菜无聊先玩会手机游戏
      李四:等着上菜无聊先玩会手机游戏
      服务员:宫爆鸡丁好了
      张三:开始吃宫爆鸡丁
      李四:开始吃宫爆鸡丁
      张三:宫爆鸡丁吃光了
      李四:宫爆鸡丁吃光了
      服务员:两位请买单

如果改用AutoResetEvent进行同步呢?会出现什么样的结果?恐怕张三和李四就要打起来了,一个享用了美味的宫爆鸡丁,另一个到要付账的时候却还在玩游戏。

原文地址:https://www.cnblogs.com/qtiger/p/5826525.html