zoukankan      html  css  js  c++  java
  • C#多线程实践-锁和线程安全

     锁实现互斥的访问,用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:
    class ThreadUnsafe {
        static int val1, val2;
        static void Go() {
            if (val2 != 0) Console.WriteLine (val1 / val2);
                val2 = 0;
        }
    }

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

      下面用c#中的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),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。因为一个线程的访问不能与另一个重叠,互斥锁有时被称之对由锁所保护的内容强迫串行化访问。在这个例子中,保护了Go方法的逻辑,以及val1 和val2字段的逻辑。一个等候竞争锁的线程被阻止将在ThreadState上为WaitSleepJoin状态。稍后将讨论一个线程通过另一个线程调用Interrupt或Abort方法来强制地被释放。这是用于结束工作线程一个相当高效率的技术。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.Ente之前却先调用了Monitor.Exit将引发异常。Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。

    选择同步对象

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

    class ThreadSafe
     {
            List <string> list = new List <string>();
     
            void Test() {
     
            lock (list) {
     
            list.Add ("Item 1");
     
            ...

      一个专门字段(如在例子中的locker)是常用的方式 , 因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:

    lock (this) { ... }

    或:

    lock (typeof (Widget)) { ... }    // 保护访问静态

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

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

    嵌套锁定

         线程可以重复锁定相同的对象,可以通过多次调用Monitor.Enter或lock语句来实现。当对应编号的Monitor.Exit被调用或最外面的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)
        {
             ... 
        }        
       // 释放了锁?没有完全释放!
    }

      线程只能在最开始的锁或最外面的锁时被阻止。

    何时进行锁定

       作为一项基本规则,任何和多线程有关的会进行读和写的字段都应当加锁。甚至是极平常的事情——单一字段的赋值操作,都必须考虑到同步问题。在下面的例子中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; }
    }

      作为加锁的另一个选择,在一些简单的情况下,也可以使用非阻止同步,将在后面讨论即使像这样的语句需要同步的原因。

     锁和原子操作

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

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

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

    性能考量

      锁本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间之前它可能花费数毫秒(千分之一秒)。相反,该使用锁而没使用的会带来更长的时间开销。如果发生了死锁和竞争锁,锁就会带来反作用,由于太多的代码被放置到锁语句中了,引起其它线程不必要的被阻止。死锁是两线程彼此等待被锁定的内容,导致两者都无法继续下去。争用锁是两个线程任一个都可以锁定某个内容,如果“错误”的线程获取了锁,则导致程序错误。

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

    线程安全

        线程安全的代码是指在面对任何多线程情况下,这代码都没有不确定的因素。线程安全首先完成锁,然后减少在线程间交互的可能性。

         一个线程安全的方法,在任何情况下可以可重入式调用。通用类型很少是线程安全的,原因如下:

      • 完全线程安全的开发是重要的,尤其是一个类型有很多字段(在任意多线程上下文中每个字段都有潜在的交互作用)的情况下。
      • 线程安全带来性能损失(要付出的,在某种程度上无论与否类型是否被用于多线程)。
      • 一个线程安全类型不一定能使程序使用线程安全,有时参与工作后者可使前者变得冗余。

         因此线程安全经常只在需要实现的地方来实现,为了处理一个特定的多线程情况。不过,有一些方法来“欺骗”,有庞大和复杂的类安全地运行在多线程环境中。一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的所有的属性、方法和字段的访问。原始类型除外,很少的.NET framework类型实例相比于并发的只读访问,是线程安全的。责任在开放人员实现线程安全代表性地使用互斥锁。另一个方式欺骗是通过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每个请求来自它自己的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法一定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,多半是在内存里缓存常用数据和提供基础设施服务,例如认证和审核。

    线程安全与.NET Framework类型

      锁定可被用于将非线程安全的代码转换成线程安全的代码。比较好的例子是在.NET framework方面,几乎所有非基本类型的实例都不是线程安全的,而如果所有的访问给定的对象都通过锁进行了保护的话,他们可以被用于多线程代码中。看这个例子,两个线程同时为相同的List增加条目,然后枚举它:

    class ThreadSafe
    {
        static List <string> list = new List <string>();
        static void Main()
        {
            new Thread (AddItems).Start();
            new Thread (AddItems).Start();
        }
     
        static void AddItems() 
        {
     
            for (int i = 0; i < 100; i++)
            lock (list)list.Add ("Item " + list.Count);
            string[] items;
            lock (list) items = list.ToArray();
            foreach (string s in items) Console.WriteLine (s);
        }
    }

        在这种情况下,我们锁定了list对象本身,这个简单的方案是很好的。如果我们有两个相关的list,也许我们就要锁定一个共同的目标——单独的一个字段,如果没有其它的list出现,显然锁定它自己是明智的选择。枚举.NET的集合也不是线程安全的,在枚举的时候另一个线程改动list的话,会抛出异常。为了不直接锁定枚举过程,在这个例子中,我们首先将项目复制到数组当中,这就避免了固定住锁因为我们在枚举过程中有潜在的耗时。

          这里的一个有趣的假设:想象如果List实际上为线程安全的,如何解决呢?代码会很少!举例说明,我们说我们要增加一个项目到我们假象的线程安全的list里,如下:

    if (!myList.Contains (newItem)) myList.Add (newItem);

      无论与否list是否为线程安全的,这个语句显然不是!(因此,可以说完全线程安全的通用集合类是基本不存在的。.net4.0中,微软提供了一组线程安全的并行集合类,但是都是特殊的经过处理过的,访问方式都经过了限定。),上面的语句要实现线程安全,整个if语句必须放到一个锁中,用来保护抢占在判断有无和增加新的之间。上述的锁需要用于任何我们需要修改list的地方,比如下面的语句需要被同样的锁包括住:

    myList.Clear();

          来保证它没有抢占之前的语句,换言之,我们必须锁定差不多所有非线程安全的集合类们。内置的线程安全,显而易见是浪费时间!

          在写自定义组件的时候,你可能会反对这个观点——为什么建造线程安全让它容易的结果会变的多余呢 ?

          有一个争论:在一个对象包上自定义的锁仅在所有并行的线程知道、并使用这个锁的时候才能工作,而如果锁对象在更大的范围内的时候,这个锁对象可能不在这个锁范围内。最糟糕的情况是静态成员在公共类型中出现了,比如,想象静态结构在DateTime上,DateTime.Now不是线程安全的,当有2个并发的调用可带来错乱的输出或异常,补救方式是在其外进行锁定,可能锁定它的类型本身—— lock(typeof(DateTime))来圈住调用DateTime.Now,这会工作的,但只有所有的程序员同意这样做的时候。然而这并靠不住,锁定一个类型被认为是一件非常不好的事情。由于这些理由,DateTime上的静态成员是保证线程安全的,这是一个遍及.NET framework一个普遍模式——静态成员是线程安全的,而一个实例成员则不是。从这个模式也能在写自定义类型时得到一些体会,不要创建一个不能线程安全的难题!

          当写公用组件的时候,好的习惯是不要忘记了线程安全,这意味着要单独小心处理那些在其中或公共的静态成员。

     
  • 相关阅读:
    Anagram
    HDU 1205 吃糖果(鸽巢原理)
    Codeforces 1243D 0-1 MST(补图的连通图数量)
    Codeforces 1243C Tile Painting(素数)
    Codeforces 1243B2 Character Swap (Hard Version)
    Codeforces 1243B1 Character Swap (Easy Version)
    Codeforces 1243A Maximum Square
    Codeforces 1272E Nearest Opposite Parity(BFS)
    Codeforces 1272D Remove One Element
    Codeforces 1272C Yet Another Broken Keyboard
  • 原文地址:https://www.cnblogs.com/qiaoke555/p/10511587.html
Copyright © 2011-2022 走看看