C#多线程之旅~上车吧?

By | 2019年1月10日

  前言:前几天,写了一篇关于多线程使用的文章,【线程使用】用法得到不少博友的好评,博主这几天加班写文章,把剩下的高级使用给写完,期望可以得到博友的追赞吧,那么废话不多说,开始我们的C#高级用法之旅!!

  前面介绍了适合在应用程序中使用线程的两种情况。但是,在一些情况下,创建新线程是不利的。在此不会列出不适台创建新线程的所有情况,只是说明一下在什么情况下不适合创建新线程。本节主要介绍两种情况:第一种情况是执行顺序极其重要,第二种情况是代码中常见的一个错误在循环中创建新线程。

再次访问的执行顺序

  从上一篇文章的交替执行的线程演示可以说明线程流是从一个线程到另一个线程随机进行的。这看起来是要先执行一个线程,在控制台中显示十行,再让一个线程显示十五行,然后返回一个线程,执行八行,决定是否使用线程的一个常见错误是,自以为知道在线程的指定时间要执行多少代码。  下面将要使用一个栗子来说明这个问题。看上去T1会先结束,因为它先启动,但这是错的,创建一个应用程序,将其对象设置为Main()。构建这几个示例,会得到不一样的结果。

   

    public class ExecutionOrder
    {
        static Thread t1;
        static Thread t2;
        public static void WriteFinished(string threadName)
        {
            switch (threadName)
            {
                case "T1":
                    Console.WriteLine();
                    Console.WriteLine("T1 Finished");
                    break;
                case "T2":
                    Console.WriteLine();
                    Console.WriteLine("T2 Finished");
                    break;
            }
        }
        public static void MainGo()
        {
            t1 = new Thread(new ThreadStart(Increment));
            t2 = new Thread(new ThreadStart(Increment));
            t1.Name = "T1";
            t2.Name = "T2";
            t1.Start();
            t2.Start();
            Console.ReadLine();
        }
        public static void Increment()
        {
            for (long i=1;i<=1000000;i++)
            {
                if (i%10000==0)
                {
                    Console.WriteLine("{"+Thread.CurrentThread.Name+"}");
                }
            }
            WriteFinished(Thread.CurrentThread.Name);
        }
    }

  有时候,线程t1先会结束,但有的时候t2会先结束,所以我们不能指定线程按照启动的先后顺序结束。

初级程序员会跳的坑

  尝到线程的甜头后,人们常犯的另一个错误是在一个循环中创建并使用线程。下面是演示这个错误的代码示例,这种代码通常是由刚刚接触到线程概念的程序员编写的。在发生一个事件时,开发人员或系统管理员通常使用这个概念来发送通知。这个想法不错,但是在循环中使用线程实现它会导致很多问题。

 public static void SendAllEmail()
        {
            int loopTo = al.count - 1;
            for (int i=0;i<=loopTo;i++)
            {
                Thread t = LoopingThreads.CreateEmail(
                     new LoopingThreads.SendMail(Mailer.MailMethod,
                     (string)[i],
                     "johndoe@somewhere.com",
                    "Thread in a joop", "Mail Example");
                t.Start();
                t.Join(Timeout.Infinite);
            }
        }

  此处省略了很多代码,因为这些测试参数非常之多,就不再写个邮箱了,我们来分析一下代码,这乍看上去,这很不错,为什么不在另一个线程上发送Email呢?它有时需要很长时间来处理,这是正确的。但问题在于来回切换线程而带来的CPU消耗。到完成这个进程为止。分配到每个线程上的时间都是来源于拆包和打包线程本地存储上了。执行线程中的指令花费了很少的时间。系统可能完全锁住,无法发送邮件。一个通用的编程惯例是将工作放到队列中。由服务器处理。例如一家银行将基XML的文件放到一个网络目录中,让在另一台服务器上运行的服务程序获得这个文化这个服务将浏览目录,查找新的文件,然后一次处理一个文件。如果-次将多个文放到目录中,服务也将依次处理文件。在一个典型的环境中,新文件很少放在这个录中。因此,乍看上去,在找到一个文件时,应启动一个新线程。这可能是对的,是,如果处理这些文件的服务程序停止了,会发生什么情况呢?如果因为网络问题很长时间里阻止服务访问这个目录,会发生什么情况呢?目录中的文件会堆积起来当再次启动服务时,或者允许服务再次访问目录时,每一个文件都会在服务上产生个新线程,这会使服务器停止运行。

  这个文件模型只是个栗子,要记住的是如果把工作放到队列中,并且要用多线程就应该使用线程池。

 为何要使用同步?

.NET 开发人员之所以需要设计多线程应用程序时保持同步,主要有两个原因。

  • 避免竞争条件
  • 确保线程的安全

  由于.NET Framework对线程提供了内置的支持,所以使所开发的类最终能够应用于多线程应用程序。所设计的每个类不必(也不需要)都是线程安全的,因为线程安的设计.NET类时,至少要考虑到线程安全。本章后面将介绍线程安全的代价以及何时使类具有线程安全性的指导原则。不必担心多线程对局部变量、方法参数和返回值的访问,因为这些变量驻留在堆栈中,本来就具备线程安全性。不过,只要很合适地设计类,实例和类变量都将是线程安全的。   举一个ATM的例子。X先生和X夫人决定从ATM.上支取1000,销掉他们的支票账号。遗憾的是他们忘记决定由谁来做此事了。更富有戏剧性的是,X先生和X夫人几乎在同时刻从不同的ATM机上进入他们的支票账户。如果两个用户同时访问同一个账户,而应用程序不是线程安全的,那么两个ATM机就有可能发现支票账户上有足够的存量,并分配给每个用户1000。两个用户触发两个线程同时访问账户数据库。在理想情况下,当一个用户试图修改其账户时,其他用户就不能在同一时刻访问该账户了。简言之,当用户访问账户,以修改与这个账户相关的信息时,该账户就应被锁定。所以至少有三种方式来保证对象的线程安全:

  • 同步代码中的重要部分
  • 使对象不可改变
  • 使用线程安全包装器

我们将要根据这三个方式来续写下文。

同步重要的代码段

  为避免多个线程在同一时刻更新资源而引起的不良结果,需要限制对资源的访问,只允许在同一时刻只有一个线程能够更新资源,换勾话说,就是使资源具有线程安全性。使对象或实例变量具有线程安全性的最简单方式是标识和同步重要的代码段。重要的代码段是指程序中多个线程可以在同一时刻访问以更新对象的状态的一段代码。例如,在上面的例子中,X夫妇二人试图同时访问同一个Withdraw()方法,Withdraw()方法就成为重要的代码段,需要具有线程安全性。最容易实施的方式是同步化方法Withdraw(),以便在任一时刻只有一个线程(X先生或X夫人)能够访问资源。在执行期间不能被中断的事务处理叫作原子操作。原子一其传统的含义是不可分的单元,原子操作的处理是作为一个完整单元执行的代码单元一就好像这些代码是一条处理器指令一样,使Withdraw()方法具有原子性一样,就可以确保在第一个线程更改完成之前,另一个线程无法去修改同一账户的余额。下面列出的是一个非线程安全的Account类的伪代码:

public class Account
    {
        public ApprovedOrNot WithDraw(Amount)
        {
            //Check...
        }
    }

下面列出了Account类的线程安全的伪代码版本:

public class Account
    {
        public ApprovedOrNot Withdraw(Amount)
        {
            lock this section(access for only one thread)
            {
                //check
            }
        }
    }

     在第一段代码中,两个或者两个以上的线程可以同时到达重要的代码,因此就有可能两个线程在同一时刻核对账户余额并分发1000美金,导致账户的透支。  而在另一段代码中,在任何时刻只能允许一个线程访问其重要代码,假设X先生来到了银行,开始进行check来验证,那么X女士再来的时候,就不能让他进行check验证,也就防止了账户透支的情况。

使账户对象不可改变

      使对象具有线程安全性的另1种方式是使对象不可改变。不可变的对象是指该对象旦创建,其状态就不能修改。只要在创建Account对象之后,不允许任何线程修改其状态,就可以实现这项操作。在这种方法中,要将读取实例变量的代码段和写入实例变量的代码段区分开。仅对读取实例变量的代码段不做改动,而对修改对象实例变量的代码段进行改变,不会修改当前对象的状态,而是会创建一个包含新状态的新对象,并返回对新对象的引用。在这种方法中,不需要锁定代码段,因为不可变对象的所有方法(只有构造函数)都不会写入对象的实例变量,所以,不可变对象在本质上就具有线程安全性。

使用线程安全包装器

  使对象具有线程安全性的第三个方法是在对象上编写一个具有线程安全性的包装器,而不是使对象本身具有线程安全性,对象保持不变,而新的包装器包括线程安全代码的同步部分。下面列出基于Account对象的包装器类:

public class AccountWrapper
    {
        private Account _a;
        public AccountWrapper(Account a)
        {
            this._a = a;
        }
        public bool Withdraw(double amount)
        {
            lock (_a)
            {
                return this._a.WithDraw(amount);
            }
        }
    }

  AccountWrapper类是Account 类的线程安全包装器,Account 实例被声明为AccountWrapper类的Private实例变量,因此其他对象或线程不能访问Account变量。在这种方法中,Account 对象不具有任何线程安全的特性,因为线程安全性是由AccountWrapper类提供的。

      在处理第三方的库,而库中的类不是为线程安全而设计时,就可以使用这种方法。例如,假设银行已经有-一个Account类,用于为它的主机系统开发软件,为了保持-致,应使用同-一个Account类来编写ATM软件。在银行提供的Account类的文档中,显然Account类不是线程安全的。另外,出于安全的原因,也不能访问Account的源代码。在这种情况下,必须采用线程安全包装器的方法,将线程安全的Accountwrapper类开发为Account类的扩展版。包装器用于将同步代码添加到非线程安全的资源中。所有的同步逻辑都在包装器类中,非线程安全的类则保持不变。

.NET 对同步的支持

  .NET Framework在System.Threading、System.EnterpriseServices和System.Runtime.Complier 命名空间上提供了一些类,当然.NET Core也是如此。程序猿可以通过这些开发线程安全。下面我们逐一谈一谈。

MethodlmplAttribute类

   System.Runtime.CompilerService命名空间包含的一些属性将影响CLR在运行期间的行为。MethodlmplAttribute就是这样的一个属性,它会告诉CLR方法是如何实现的。MethodlmplAttribute的一个构造函数把MethodImplOptions枚举作为其参数。MethodImplOptions枚举有一个字段Synchronized,它在任一时间只允许一个线程来访问这个方法。这类似于我们的lock关键字,下面说明了这个属性来同步方法(创建MI.cs):

public class MI
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void DoSomeWorkSync()
        {
            Console.WriteLine("DoSomeWorkSync()"
                               +"--Lock held by Thread"
                               +Thread.CurrentThread.GetHashCode());
            Thread.Sleep(5*1000);
            Console.WriteLine("DoSomeWorkSync()"
                               + "--Lock held by Thread"
                               + Thread.CurrentThread.GetHashCode());
        }
        public void DoSomeWorkNoSync()
        {
            Console.WriteLine("DoSomeWorkNoSync()"
                               + "--Lock held by Thread"
                               + Thread.CurrentThread.GetHashCode());
            Thread.Sleep(5 * 1000);
            Console.WriteLine("DoSomeWorkNoSync()"
                               + "--Lock held by Thread"
                               + Thread.CurrentThread.GetHashCode());
        }
    }
class Program
    {
        static void Main(string[] args)
        {
            MI m = new MI();
            Thread t1 = new Thread(new ThreadStart(m.DoSomeWorkNoSync));
            Thread t2 = new Thread(new ThreadStart(m.DoSomeWorkNoSync));
            t1.Start(); t2.Start();
            Thread t3 = new Thread(new ThreadStart(m.DoSomeWorkSync));
            Thread t4 = new Thread(new ThreadStart(m.DoSomeWorkSync));
            t3.Start(); t4.Start();
        }
    }

上述代码的输出如下所示(计算机不同可能结果就不同,因为进程的ID不同)

      在上面的代码中,MI类有两个方法doSomeWorkSync0和doSomeWorkNoSync()。MethodImpl属性应用于doSomeWorkSync()方法, 用于同步该方法;而doSomeW orkNoSync()保持不变,允许多个线程同时访问方法。在Main0方法中,线程t1和t2访问未同步的方法,线程t3和t4访问已同步的方法。在这两个方法中,都添加了Thread,Sleep(方法, 用于为另一个正在等待的线程提供足够的时间进入方法,同时第一个线程仍在方法中。如果程序的情况是这样,线程t1和t2就可以同步进入doSomeWorkNoSync()方法,而线程t3和t4中只有一个可以进入doSomeWorkSync()方法。如果tl和t2有相同的优先级,则哪一个线程会优先执行就是随机的; .NETFramework不保证线程执行的顺序。

      仔细研究一下输出, 注意线程2(t1)和线程3(t2)同时进入doSomeW orkNoSync0方法,而”且线程4(t3)获得了doSomeWorkSync()的锁定,线程5(t4)就不能进入该方法了,只有线程4(t3)释放了该方法的锁定,线程5(t4)才能再次获得该锁定。

.NET同步策略

公共语言基础结构提供了3种策略同步去访问示例、静态方法和示例字段,分为以下三种:

  • 同步上下文
  • 同步代码区
  • 手控同步

同步上下文

  上下文是一组属性或使用规则,这种属性和使用规则对执行时相关的对象集合是通用的。能够添加的上下文属性包括有关同步、线程亲缘性和事务处理的策略,简而言之,上下文把类似的对象组合在一起,这种策略将使用Synchronization 类对ContextBoundObject对象进行自动同步。由于线程同步和并发管理是开发人员遇到最头疼的事情也是最困难的任务,因此这种方法是极大提高了效率。  Synchronization 类对缺少手工处理同步经验的程序员来说是非常有用的。包括我。因为它包括了实例变量,实例方法和应用这个属性的类的实例字段。然而。它不处理静态字段和方法的同步。如果必须同步制定代码块。它不起作用。同步整个对象是对轻松使用必须付出的代价。在使用System.EnterpriseAttribute编程时,Synchronization 非常方便,因为一个上下文的对象由com+运行库组合在一起。

  回到前面的栗子里,Account这个示例,可以使用Synchronization 将伪代码变成具有安全性的线程。下面使用了它进行同步Account类:

 [SynchronizationAttribute(SynchronizationAttribute.REQUIRED)]
    public class Account : ContextBoundObject
    {
        public ApprovedOrNot Withdraw(Amount)
        {
            //check
        }
    }

同步代码区

  第二种同步策略是特定代码区的同步,这些特定的代码区是方法种的重要代码段。它们可以改变对象的状态,或者更新另一资源。下面我们介绍Monitor和ReadWriterLock类。

Monitor类

  Monitor 用于同步代码区,其方式是使用Monitor.Enter()方法获得一个锁,然后,使用Monitor.Exit()方法释放该锁,锁的概念通常用于解释Monitor类。一个线程获得锁。其他线程就要等到被释放之后才可以使用。一旦在代码中获得了锁,就可以通过Monitor.Enter()和Monitor.Exit()程序块。

  • Wait() —— 此方法用于释放对象上的锁,并暂停当前线程,知道它重新获得了锁。

  • Pulse() —— 此方法用于通知正在队列中等待的线程,对象的状态已经改变了。

  • PulseAll() —— 此方法用于通知所有正在队列中等待的线程,对象的状态已经改变了。

      注意,Monitor 方法是静态方法,能被Monitor类自身调用,而不是由该类的实例调用。在.NET Framework中,每个对象都有一个与之相关的锁,可以获取和释放该锁,这样,在任一时刻仅有一个线程可以访问对象的实例变量和方法。与之类似,.NETFramework中的每个对象也提供一种允许它处于等待状态的机制。正如锁的机制,设计这种机制的主要原因是帮助线程间的通信。如果一个线程进入对象的重要代码段,并需要一定的条件才能存在,而另一线程可以在该代码段中创建该条件,此时就需要这种机制,下面是使用Enter()和Exit()方法的示例。

    public class MonitorEnterExit
    {
        private int result = 0;
        public MonitorEnterExit()
        {
        }
        public void NonCriticalSection()
        {
            Console.WriteLine("Entered Thread"+Thread.CurrentThread.ManagedThreadId);
            for (int i=0;i<=5;i++)
            {
                Console.WriteLine("Result="+result+++"ThreadID:"+Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1000);
            }
            Console.WriteLine("Exiting Thread"+Thread.CurrentThread.GetHashCode());
        }
        public void CriticalSection()
        {
            //Enter the Critical Section
            Monitor.Enter(this);
            Console.WriteLine("Enter Thread"+ Thread.CurrentThread.GetHashCode());
            for (int i = 0; i <= 5; i++)
            {
                Console.WriteLine("Result=" +result+++ "ThreadID:" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1000);
            }
            Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
            Monitor.Exit(this);
        }
    }

上述代码的输出如下所示(计算机不同可能结果就不同,因为进程的ID不同)

  比较一下重要代码和非重要代码的输出结果,就会使代码变得清晰,两个线程都修改了result变量,产生一个混合变量型输出结果,这是因为在NonCriticalSection方法中没有锁,这个线程不是安全的,多个线程可以访问方法。同时局部变量可以访问方法。想法我们看一下CriticalSection方法,在第一个线程退出之前,其他的线程都无法访问重要的代码段,那就这么锁住了,怎么解开呢?请再读下文!

 Wait()和Pulse()机制

   Wait()和Pulse{)机制用于线程间的交互。当在一个对象.上执行Wait()时,正在访问该对象的线程就会进入等待状态,直到它得到一个唤醒的信号。Pulse()和PulseAll()用于给等待线程发送信号。下面列出的是一个Wait{)和Pulse()方法如何工作的例子,即WaitAndPulse.cs示例:

注意:使用这两个方法一定要在锁中!!!

public class LockMe{}

    public class WaitPulse1
    {
        private int result = 0;
        private LockMe _IM;
        public WaitPulse1()
        {
        }
        public WaitPulse1(LockMe l)
        {
            this._IM = l;
        }
        public void CriticalSection()
        {
            Monitor.Enter(this._IM);
            Console.WriteLine("WaitPulsel:Entered Thread" + Thread.CurrentThread.ManagedThreadId);
            for (int i=1;i<=5;i++)
            {
                Monitor.Wait(this._IM);
                Console.WriteLine("WaitPulsel:WorkUp");
                Console.WriteLine("WaitPulsel:Result="+result+++"ThreadID"+Thread.CurrentThread.GetHashCode());
                Monitor.Pulse(this._IM);
            }
            Console.WriteLine("WaitPulsel:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
            Monitor.Exit(this._IM);
        }
    }

    public class WaitPulse2
    {
        private int result = 0;
        private LockMe _IM;
        public WaitPulse2()
        {
        }
        public WaitPulse2(LockMe l)
        {
            this._IM = l;
        }
        public void CriticalSection()
        {
            Monitor.Enter(this._IM);
            Console.WriteLine("WaitPulse2:Entered Thread" + Thread.CurrentThread.ManagedThreadId);
            for (int i = 1; i <= 5; i++)
            {
                Monitor.Pulse(this._IM);
                Console.WriteLine("WaitPulse2:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
                Monitor.Pulse(this._IM);
                Console.WriteLine("WaitPulse2:WorkUp");
            }
            Console.WriteLine("WaitPulse2:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
            Monitor.Exit(this._IM);
        }
    }
static void Main(string[] args)
        {
            LockMe lockMe = new LockMe();
            WaitPulse1 el = new WaitPulse1(lockMe);
            WaitPulse2 e2 = new WaitPulse2(lockMe);
            Thread t1 = new Thread(new ThreadStart(el.CriticalSection)); t1.Start();
            Thread t2 = new Thread(new ThreadStart(e2.CriticalSection)); t2.Start();
            Console.ReadLine();
        }

上述代码的输出如下所示

 

      在Main()方法中,创建一个名为1的LockMe对象。接着创建两个类型为WaitPulse!和WaitPulse2的对象,然后把它们作为委托传递,以便线程能够调用这两个对象的CriticalSection()方法。注意,WaitPulsel 中的LockMe 对象实例与WaitPulse2 中的LockMe对象实例相同,因为对象按引用被传递给它们各自的构造函数。初始化对象后,创建两个线程t1和t2,把它们分别传递给两个CriticalSection方法。

      假设WaitPulse1.CriticalSection(首先获得调用,则线程t1通过LockMe对象.上的锁定进入该方法的重要代码段,然后在For循环中执行Monitor.Wait()。执行Monitor.Wait()方法后,线程tl等待另一个线程的运行时通知(Monitor.Pulse()),而被唤醒。锁定LockMe对象是希望在任一时刻都只允许一个线程访问共享的LockMe实例。

      注意,当线程执行Monitor. Wait(方法时,它会临时释放LockMe对象上的锁,以便其他线程可以访问它。线程t1进入等待状态后,线程t2就可自由访问LockMe对象。即使LockMe对象是个独立的对象((WaitPulsel和WaitPulse2),这两个线程也均指向同一个对象引用。线程t2获得LockMe对象上的锁后,进入WaitPulse2.CriticalSection()方法。它一进入For循环,就给在LockMe对象上等待的线程(此时是t1)发送-一个运行时通知(Monitor.Pulse()),之后进入睡眠状态。结果t1醒来,获得LockMe对象上的锁。接着线程t1访问result变量,并给在LockMe对象.上等待的线程(此时是t2)发送-一个运行时通知。这个循环一直持续到For循环结束。.

      如果把程序的输出结果和上面的描述相对比,概念就非常明确了。注意每个Enter()方法都应该伴随一个Exit()方法,否则程序将陷入死循环。

TryEnter()方法

  Monitor类的TryEnter()方法非常类似于Enter()方法,它视图获得对象的独占楼,不过它不会想Enter()方法那样暂停。如果线程成功进入,则TryEnter()方法返回True。

public class MonitorTryEnter
    {
        public MonitorTryEnter()
        {
        }
        public void CriticalSection()
        {
            bool b = Monitor.TryEnter(this,1000);
            Console.WriteLine("Thread=" 
                + Thread.CurrentThread.GetHashCode()+"TryEnter Value"+b);
            for (int i=1;i<=3;i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine(i+" "+Thread.CurrentThread.GetHashCode()+" ");
            }
            Monitor.Exit(this);
        }
    }

上述代码的输出如下所示

  有可能会发生冲突,那也是无法避免的,如果真的需要知道是否被锁定,也只能这么干。现在到了这里,我觉得你已经写的不耐烦了,包括我。。。现在我们使用lock关键字,这个关键字替代了Monitor,和上面部分代码是等价的。

Monitor.Enter(x) … Monitor.Exit(x) == lock(x){…}

private int result = 0;
        public void CriticalSection()
        {
            lock (this)
            {
                Console.WriteLine("Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
                for (int i=1;i<=5;i++)
                {
                    Console.WriteLine("Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
                    Thread.Sleep(1000);
                }
                Console.WriteLine("Exiting Thread"+ Thread.CurrentThread.GetHashCode());
            }
        }

这是一个最基本的lock用法,除此之外还有一个ReaderWriterLock类

ReadWriteLock

  ReaderWriterLock 定义了实现单写程序和多读程序语义的锁,这个类主要用于文件操作,即多个线程可以读取文件,但只能一个线程进行更新文件。ReaderWriterLock 类中4个主要方法是:

  • AcquireReaderLock():该重载方法获得一个读程序锁,超时值使用整数或TimeSpan。超时是用于检测死锁的好工具。
  • AcquireWriterLock():该重载方法获得了一个写程序锁,超时值使用整数或TimeSpan。
  • ReleaseReaderLock():释放读程序锁
  • ReleaseWriterLock():释放写程序锁

  使用ReaderWriterLock 类时,任意数量的线程都可以同时安全地读取数据。只有当线程进行更新时,数据才被锁定。只有在没有占用锁的写程序线程时,读程序线程才能获得锁。只有在没有占用锁的读程序或写程序线程时,写程序线程才能获得锁。

 下面给出程序ReadWriteLock.cs,展示它们的用法。
 

    public class ReadWrite
    {
        private ReaderWriterLock rwl;
        private int x;
        private int y;
        public ReadWrite()
        {
            rwl = new ReaderWriterLock();
        }
        public void ReadInts(ref int a,ref int b)
        {
            rwl.AcquireReaderLock(Timeout.Infinite);
            try
            {
                a = this.x;
                b = this.y;
            }
            finally
            {
                rwl.ReleaseReaderLock();
            }
        }
        public void WriteInts(int a,int b)
        {
            rwl.AcquireWriterLock(Timeout.Infinite);
            try
            {
                this.x = a;
                this.y = b;
                System.Console.WriteLine("x="+this.x+"y="+this.y+
                        "ThreadID="+Thread.CurrentThread.GetHashCode());
            }
            finally
            {
                rwl.ReleaseWriterLock();
            }
        }
    }
    class Program
    {
        public ReadWrite rw = new ReadWrite();
        static void Main(string[] args)
        {
            Program program = new Program();
            //writer thraeds
            Thread wt1 = new Thread(new ThreadStart(program.Writer));wt1.Start();
            Thread wt2 = new Thread(new ThreadStart(program.Writer));wt2.Start();
            //reader threads
            Thread rt1 = new Thread(new ThreadStart(program.Read));rt1.Start();
            Thread rt2 = new Thread(new ThreadStart(program.Read));rt2.Start();
        }
        private void Writer()
        {
            int a = 10;
            int b = 11;
            Console.WriteLine("*****write*****");
            for (int i = 0; i < 5; i++)
            {
                this.rw.WriteInts(a++,b++);
                Thread.Sleep(1000);
            }
        }
        private void Read()
        {
            int a = 10;
            int b = 11;
            Console.WriteLine("*****raed*****");
            for (int i=0;i<5;i++)
            {
                this.rw.ReadInts(ref a,ref b);
                System.Console.WriteLine("x=" + a + "y=" + b +
                       "ThreadID=" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1000);
            }
        }
    }
 上述代码的输出如下所示

  监控器对于仅计划读取数据而不修改数据的线程来说可能“太安全”了。在这方面,监控器的性能有所下降,而且,对于只读类型的访问,这种性能的降低不是必需的。ReaderWriterLock类允许任意多的线程同时读取数据,提供了一种处理数据读写访问的最佳解决方案。仅当线程更新数据时,它才锁定数据。当且仅当没有写程序线程占用锁时,读程序线程才能获得锁。当且仅当没有读程序或写程序线程占用锁时,写程序线程才能获得锁。因此,ReaderWriterLock 类与重要代码段是一样的。除此之外,该类还支持一个可用于检测死锁的超时值。

手控同步

  第三个策略用到了手控同步,而.NET Framework提供了一套经典的技术,允许程序员类似于win32线程API的低级API创建和管理线程。

ManualResetEvent 类

  ManualResetEvent对象只能拥有两种状态之一。有信号或者无信号,是个bool值。它继承于WaitHandle 类,其构造函数的参数可确定对象的初始状态。Set()和Reset()方法返回一个bool值,表示是否成功修改。下面列出了相关栗子。

static void Main(string[] args)
        {
            ManualResetEvent manual;
            manual = new ManualResetEvent(false);
            Console.WriteLine("稍等会!");
            bool b = manual.WaitOne(1000,false);
            Console.WriteLine("我靠我来了!"+b);
        }
上述代码的输出如下所示

  程序块在WaitOne()方法中暂停一秒,然后因为超时而退出,ManualResetEvent 的状态仍然是false,因而返回的值是false,现在我们把它改成true,试试瞧!!!

static void Main(string[] args)
        {
            ManualResetEvent manual;
            manual = new ManualResetEvent(true);
            Console.WriteLine("稍等会!");
            bool b = manual.WaitOne(1000,true);
            Console.WriteLine("我靠我来了!"+b);
        }

      将ManualResetEvent的初始状态修改为有信号的,即使指定超时值为1000亳秒,线程在WaitOne()中也不会等待。当ManualResetEvent的状态为无信号状态时,线程将等待状态变为有信号状态,但1000毫秒后线程超时。状态已经是有信号的,  因而线程没理由在WaitOne()方法中等待。为了把ManualResetEvent的状态修改为无信号的,必须调用ManualResetEvent的Reset()方法;为了把状态修改为有信号的,必须调用Set()方法。
   下面列出Reset()方法的使用,再举个栗子(实在写不动了,累了,明天写…)
      好,经过了一晚上的休息,今天早上继续干!。。。
[STAThread]
        static void Main(string[] args)
        {
            ManualResetEvent manualRE;
            manualRE = new ManualResetEvent(true);
            bool b = manualRE.WaitOne(1000,true);
            Console.WriteLine("稍等会"+b);
            manualRE.Reset();
            b = manualRE.WaitOne(5000,true);
            Console.WriteLine("中了不" + b);
        }
 上述代码的输出如下所示

   在ManualReset 中,ManualResetEvent 对象的构造函数将其状态设置为有信号的(True),结果,线程不在第一个WaitOne()方法中 等待,并返回True 值。接着将ManualResetEvent对象的状态重新设置为无信号的(False),于是线程在超时之前必须等待5秒钟。
  我们再试一下Set()方法。

[STAThread]
        static void Main(string[] args)
        {
            ManualResetEvent manualRE;
            manualRE = new ManualResetEvent(true);
            bool b = manualRE.WaitOne(1000,true);
            Console.WriteLine("稍等会"+b);
            manualRE.Set();
            b = manualRE.WaitOne(5000,true);
            Console.WriteLine("中了不" + b);
        }

WaitOne是等待一个事件有信号,拿WaitAll是等待所有时间对象都有消息才可以的。这里就不再演示了。

AutoResetEvent 类

AutoResetEvent和ManualResetEvent类差不多,它等待时间超时或者事件变成有信号状态,接着将此事件通知给等待线程。说白了这俩的区别就是,咱们这个的WaitOne方法是直接改变状态的了。呵呵。

[STAThread]
        static void Main(string[] args)
        {
            AutoResetEvent aRe;
            aRe = new AutoResetEvent(true);
            Console.WriteLine("Before First WaitOne");
            bool state = aRe.WaitOne(1000,true);
            Console.WriteLine("After First WaitOne"+state);
            state = aRe.WaitOne(5000,true);
            Console.WriteLine("After Second WaitOne" + state);
        }

同步和性能

      要获得同步锁,就需要一定的系统时间。其结果是,性能总是弱于非线程安全版本。当许多线程试图同时访问对象以获得同步锁时,整个应用系统的性能就会受到影响。当设计大型应用程序时,开发人员必须权衡这一点。重要的是,在执行一个彻底的测试之前,这些线程之间的竞争是不可见的。在设计大规模的多线程应用程序时,测试是非常重要的。开发人员必须权衡这些因素:

      ●为安全起见,尽可能实现同步。这将使程序更慢,更糟糕的是,还没有单线程快。。

      ●为了性能,尽可能减少同步。

   尽管线程安全性非常重要,但是如果不使用地使用同步,就可能会引起死锁,如果避免死锁和什么是死锁,还是要谈一谈的。例如程序中的循环就可能会引起死锁。


注:www2014.aspxhtml.com转载自cnblogs,若看不到图片,请移步来源网址查看。
via:https://www.cnblogs.com/ZaraNet/p/10248350.html

发表评论

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