C#线程(二)多线程

本文深入探讨了C#中的多线程,包括锁的使用、线程安全的概念,详细讲解了Monitor、Mutex、AutoResetEvent等同步机制。通过实例展示了如何避免死锁,以及如何实现线程同步和线程安全的代码。此外,还介绍了ReaderWriterLockSlim类在读写操作中的应用,以及在多线程应用中的任务确认和生产者/消费者队列问题的解决方案。
摘要由CSDN通过智能技术生成

3.多线程

3.1. 锁和线程安全

锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:

class ThreadUnsafe

{

    static int val1, val2;

    static void Go()

    {

          if (val2 != 0) Console.WriteLine (val1 /val2);

            val2= 0;

     }

}

   这不是线程安全的:如果Go方法被两个线程同时调用,可能会得到在某个线程中除数为零的错误,因为val2可能被一个线程设置为零,而另一个线程刚好执行到if和Console.WriteLine语句。

   下面用lock来修正这个问题:

class ThreadSafe {

     static object locker= new object();

     static int val1,val2;

     static void Go() {

         lock (locker) {

            if (val2 != 0) Console.WriteLine(val1 / val2);

              val2 = 0;

          }

    } 

}

在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。互斥锁有时被称之对由锁所保护的内容强迫串行化访问,因为一个线程的访问不能与另一个重叠。

    一个等候竞争锁的线程被阻止其状态为WaitSleepJoin状态。

C#的lock 语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:

Monitor.Enter (locker);

try {

    if (val2 != 0)Console.WriteLine (val1 / val2);

   val2 = 0;

}

finally {

 Monitor.Exit(locker);

}

在同一个对象上,在调用第一个之前Monitor.Enter而先调用了Monitor.Exit将引发异常。

Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false,因为超时了。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。

3.1.1. 选择同步对象

任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。服从这些规则,同步对象可以兼对象和保护两种作用。比如下面List :

class ThreadSafe {

    List <string> list = new List<string>();

    void Test() {

        lock (list) {

            list.Add ("Item 1");

...

一个专门字段是常用的,因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:

lock (this) { ... }

是不好的,因为这潜在的可以在公共范围访问这些对象。

    锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x)而被阻止,两者都要调用lock(x) 来完成阻止工作。

3.1.2. 嵌套锁定

线程可以重复锁定相同的对象,可以通过多次lock语句来实现。线程只能在最开始的锁或最外面的锁上被阻止。当最外面的lock语句完成后,对象那一刻被解锁。

static object x = new object();

static void Main() {

    lock (x) {

      Console.WriteLine("I have the lock");

        Nest();

      Console.WriteLine("I still have the lock");

   }

        在这锁被释放

}

static void Nest() {

    lock (x) {

         ...

    }            释放了锁?没有完全释放!

}

3.1.3. 何时进行锁定

作为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。在下面的例子中Increment和Assign 都不是线程安全的:

class ThreadUnsafe {

    static int x;

    static void Increment() { x++; }

    static void Assign() { x = 123; }

}

  下面是Increment 和 Assign 线程安全的版本:

class ThreadUnsafe {

    static object locker =new object();

    static int x;

    static void Increment() {lock (locker) x++; }

    static void Assign() {lock (locker) x = 123; }

 }

   

3.1.4. 锁和原子操作 

    如果有很多变量在一些锁中总是进行读和写的操作,那么你可以称之为原子操作。我们假设x 和 y不停地读和赋值,他们在锁内通过

locker锁定:

    lock (locker) { if (x != 0) y /= x; }

   你可以认为x 和 y 通过原子的方式访问,因为代码段没有被其它的线程分开或抢占,别的线程改变x和 y是无效的输出,你永远不会得到除数为零的错误,保证了x 和 y总是被相同的排他锁访问。

3.1.5. 性能考量

   锁定本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间之前它可能花费数毫秒(千分之一秒)。而相反,与此相形见绌的是该使用锁而没使用的结果就是带来数小时的时间,

  对于太多的同步对象死锁是非常容易出现的症状,一个好的规则是开始于较少的锁,在一个可信的情况下涉及过多的阻止出现时,增加锁的粒度。

3.1.6. 死锁

死锁是指多个线程共享某些资源时,都占用一部分资源,而且都在等待对方释放另一部分资源,从而导致程序停滞不前的情况。如下面的例子。

private static object o1 = new object();

private static object o2 = new object();

private static void Work1(){

       lock(o1){

              …

              lock(o2){

                     …

               }

        }

}

private static void Work2(){

       lock(o2){

              …

              lock(o1){

                     …

            }

        }

}

从上面的例子可以看出,当t1、t2线程分解调用Work1与Work2时。t1进入lock(o1)此时t2进入lock(o2),t1请求lock(o2)时会等待t2释放o2,t2请求lock(o1)时会等待t1释放o1,因此t1与t2相互等待,形成死锁。

 

 

3.2. 线程同步

    lock语句(也称为Monitor.Enter / Monitor.Exit)是线程同步结构的一个例子。当lock对一段代码或资源实施排他访问时, 但有些同步任务是相当笨拙的或难以实现的,比如说需要传输信号给等待的工作线程使其开始任务执行。

    Win32 API拥有丰富的同步系统,这在.NET framework以EventWaitHandle,Mutex 和 Semaphore类展露出来。而一些比有些更有用:例如Mutex类,在EventWaitHandle提供唯一的信号功能时,大多会成倍提高lock的效率。

   这三个类都依赖于WaitHandle类,尽管从功能上讲, 它们相当的不同。但它们做的事情都有一个共同点,那就是,被“点名”,这允许它们绕过操作系统进程工作,而不是只能在当前进程里绕过线程。

   EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。这两个类都派生自它们的基类:它们仅有的不同是它们用不同的参数调用基类的构造函数。性能方面,使用Wait Handles系统开销会花费在微秒间,不会在它们使用的上下文中产生什么后果。

   AutoResetEvent在WaitHandle中是最有用的的类,它连同lock语句是一个主要的同步结构。

 

3.2.1. Synchronization

Synchronization属性和ContextBoundObject类,这两个一起使用可以让一个类的实例处于同步环境中,注意不需要写一大堆lock,只需要在类上有Synchronization这个属性和继承ContextBoundObject,这种数据同步方式简单粗暴,简单是因为只需要做一个声明即可,粗暴是因为在使用该类中的所有数据成员和方法时,都会被锁定,要知道,有时候并不是类中所有的成员,所有的情形都需要进行数据同步的,这就可能是一个严重的性能问题了。

namespace ConsoleApplication2

{

    class Program

    {

        [Synchronization(true)]

        class Test :ContextBoundObject

        {

            public  int count = 0;

            public voidDisplay()

            {

                count++;

               Console.WriteLine("ContextID:{1},统计:{0}", count, Thread.CurrentContext.ContextID);

            }

        }

 

        static voidMain(string[] args)

        {

            Test test = newTest();

            Thread thread1 =new Thread(new ThreadStart(() =>

            {

                for (int i =0; i < 50; i++)

                {

                   test.Display();

                }

            }));

 

            Thread thread2 =new Thread(new ThreadStart(() =>

            {

                for (int i =0; i < 50; i++)

                {

                    test.Display();

                }

            }));

 

            thread1.Start();

            thread2.Start();

            Console.ReadKey();

        }

    }

}

这样输出的数是连续的。

 

3.2.2. Lock

lock关键字很简单,简单的解析就是一把锁,当有一个线程持有这把锁时,其他线程不能进入这个“锁”区域,C#中表现这个区域为一个{}块,称作临界区。

class ThreadSafe {

     static object locker= new object();

     static int val1,val2;

     static void Go() {

         lock (locker) {

            if (val2 != 0) Console.WriteLine(val1 / val2);

              val2 = 0;

          }

    } 

}

3.2.3. Mutex

Mutex称作互斥锁,它可以等待一处的代码同步,也可以等待多处代码同步。作用类似于Lock,Mutex使用WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放。Mutex是相当快的,而lock 又要比它快上数百倍,获取Mutex需要花费几微秒,获取lock需花费数十纳秒。

Mutex是线程相关的,即当一个线程调用WaitOne获得Mutex所有权后,只有该线程能够通过ReleaseMutex()方法释放Mutex的所有权,如果一个线程获得了Mutex的所有权,其他线程调用了该Mutex对象的ReleaseMutex()<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值