• 【汇总】C#编程技巧


    拷贝/克隆(深度/浅拷贝)、序列化、反射

    Json/Xml

    窗体的单例模式

    https://www.cnblogs.com/imstrive/p/5426503.html

    使用XmlSerializer序列化可空属性

    http://zz8ss5ww6.iteye.com/blog/1123842

    VS强大插件-Reshaper使用有感

    http://www.360doc.com/content/17/1104/03/1353678_700726432.shtml

    c# is和as的区别

    http://www.cnblogs.com/ziling8163/articles/891837.html

    Winform开发之ComboBox和ComboBoxEdit控件绑定key/value数据

    https://www.cnblogs.com/Brambling/p/7114203.html

    C#基础知识-XML介绍及基本操作(十)

    https://www.cnblogs.com/leonliuyifan/p/7044438.html

    归纳一下:C#线程同步的几种方法

    https://www.cnblogs.com/michaelxu/archive/2008/09/20/1293716.html

     

    一、浅拷贝和深拷贝

    有两种对象克隆的方法:浅拷贝和深拷贝。浅拷贝只是复制引用,而不会复制引用的对象。深拷贝会复制引用的对象。

    因此,原始对象中的引用和浅拷贝对象中的同一个引用都指向同一个对象。而深拷贝的对象包含了对象的一切直接或间接的引用。参看维基百科(http://en.wikipedia.org/wiki/Object_copy)来获得更多解释。

    ICloneable接口

    ICloneable接口包含一个Clone方法,可以用来创建当前对象的拷贝。

    public interface ICloneable
    {
    object Clone();
    }


    ICloneable的问题是Clone方法并不会显式地指定是执行浅拷贝或深拷贝,因此调用者将无法确定实际情况。因此,有一些关于把ICloneable从.NET框架中淘汰的讨论。MSDN文档似乎暗示Clone方法是进行的深拷贝,但是文档没有明确的说明:

    ICloneable接口包含一个成员方法,Clone,意在支持超过MemberWiseClone所提供的功能... MemberWiseClone进行的是浅拷贝...

    类型安全的克隆

    ICloneable的另一个缺点是Clone方法返回的是一个对象,因此每次调用Clone都要进行一次强制类型转换。

    Person joe = new Person();
    joe.Name = "Joe Smith";
    Person joeClone = (Person)joe.Clone();


    一种可以避免进行强制类型转换的方式是提供你自己的类型安全的Clone方法。注意,你依然要提供ICloneable.Clone方法的以满足iCloneable接口的要求。

    public class Person : ICloneable
    {
    public string Name;
    object ICloneable.Clone()
    {
    return this.Clone();
    }
    public Person Clone()
    {
    return (Person)this.MemberwiseClone();
    }
    }
    MemberwiseClone() 方法创建一个浅表副本,具体来说就是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其复本引用同一对象。

    选择克隆方法

    1. 手工克隆

    一个能够保证对象完全按照你所想的那样进行克隆的方式是手工克隆对象的每一个域(field)。这种方式的缺点是麻烦而且容易出错:如果你在类中增 加了一个域,你很可能会忘记更新Clone方法。还要在克隆引用对象指向原始对象的时候,注意避免无限循环引用。下面是一个进行深拷贝的简单例子:

    public class Person : ICloneable
    {
    public string Name;
    public Person Spouse;
    public object Clone()
    {
    Person p = new Person();
    p.Name = this.Name;
    if (this.Spouse != null)
    p.Spouse = (Person)this.Spouse.Clone();
    return p;
    }
    }

    2. 使用MemberWiseClone方法

    MemberWiseClone是Object类的受保护方法,能够通过创建一个新对象,并把所有当前对象中的非静态域复制到新对象中,从而创建一 个浅拷贝。对于值类型的域,进行的是按位拷贝。对于引用类型的域,引用会被赋值而引用的对象则不会。因此,原始对象及其克隆都会引用同一个对象。注意,这 种方法对派生类都是有效的,也就是说,你只需在基类中定义一次Clone方法。下面是一个简单的例子:

    public class Person : ICloneable
    {
    public string Name;
    public Person Spouse;
    public object Clone()
    {
    return this.MemberwiseClone();
    }
    }

    3. 用反射进行克隆

    用反射进行克隆是使用Activator.CreateInstance方法来创建一个相同类型的新对象,然后用反射对所有域进行浅拷贝。这种方法 的优点是它是全自动的,不需要在对象中添加或删除成员的时候修改克隆方法。另外它也能被写成提供深拷贝的方法。缺点是使用了反射,因此会比较慢,而且在部 分受信任的环境中是不可用的。示例代码

    4. 使用序列化进行克隆

    克隆一个对象的最简单的方法是将它序列化并立刻反序列化为一个新对象。和反射方法一样,序列化方法是自动的,无需在对对象成员进行增删的时候做出修 改。缺点是序列化比其他方法慢,甚至比用反射还慢,所有引用的对象都必须是可序列化的(Serializable)。另外,取决于你所使用的序列化的类型 (XML,SOAP,二进制)的不同,私有成员可能不能像期望的那样被克隆。示例代码在这里,这里和这里。

    序列化克隆的一种   System.SerializableAttribute

    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;

    namespace Sample

    {

    [Serializable]

    public class Person : ICloneable
    {

      public string Name;
      object ICloneable.Clone()
      {
      return this.Clone();
      }
      public Person Clone()
      { 

        using(MemoryStream ms = new MemoryStream())

        {
          object cloneObject;
          BinaryFormatter bf = new BinaryFormatter(null,new StreamingContext(StreamingContextStates.Clone));
          bf.Serialize(ms,this);
          ms.Seek(0,SeekOrigin.Begin);

          cloneObject = bf.Deserialize(ms);
          ms.Close();
          return (Person)cloneObject;
        }

      } 

    }

    }

    5. 使用IL进行克隆

    一种罕见的解决方案是使用IL(中间语言)来进行对象克隆。这种方式创建一个动态方法(DynamicMethod),获取中间语言生成器 (ILGenerator),向方法中注入代码,把它编译成一个委托,然后执行这个委托。委托会被缓存,因此中间语言只在初次克隆的时候才会生成,后续的 克隆都不会重新生成一遍。尽管这种方法比使用反射快,但是这种方法难以理解和维护。示例代码

    6. 使用扩展方法进行克隆

    Havard Stranden用扩展方法(extention method)创建了一个自定义的克隆框架。这个框架能够创建对象及其引用的对象的深拷贝,不管对象结构有多复杂。缺点是,这是一个不提供源代码的自定义 框架(更新:现在已经包括源代码了,参见本文评论),并且它不能在不使用无参数构造器的时候,拷贝由私有方法创建的对象。另一个问题,也是所有自动化的深 克隆方法共有的问题是,深拷贝通常需要灵活地处理不能进行简单自动化特殊情况(例如未受管理的资源)。

    二、数据库操作

    1、在使用DataTable.select进行排序时,直接写DataRow[] drr = dt.Select("POSTPARID='" + node.Value.ToString() + "' order by  DISPINDEX ASC");

    这样写会报如题错误:“order”运算符后缺少操作数。

    改成DataRow[] drr = dt.Select("POSTPARID='" + node.Value.ToString() + "'", " DISPINDEX ASC")即可

    三、自定义属性

    描述

    属性提供功能强大的方法以将声明信息与 C# 代码(类型、方法、属性等)相关联。属性与程序实体关联后,即可在运行时使用名为“反射”的技术查询属性。

    属性以两种形式出现:

    • 一种是在公共语言运行库 (CLR) 中定义的属性。

    • 另一种是可以创建的用于向代码中添加附加信息的自定义属性。此信息可在以后以编程方式检索。

    自定义属性的作用

          有时候我们需要给一个类或者类中的成员加上一些属性或者附加信息,让类或者变量的功能更明确可控制的细粒度更高,打个简单的比方:数据库里面的一张表,表中的每一个字段都有很多属性,如是否主键,默认值,注释信息等等,我们在编写实体类的时候,如何表示这些信息呢?通过自定义属性可以实现。

    自定义属性的实现步骤

    1、声明一个类,并将 AttributeUsageAttribute 属性应用到该类中。类的名称即为新属性的名称 
    2、声明该类从 System.Attribute 继承: 
    3、定义 Private 字段来存储属性值: 
    4、需要时,请为属性创建构造函数: 
    5、为属性 (Attribute) 定义方法、字段和属性 (Property):

     实例一个:

    属性类(和相关枚举)

    /// <summary>
        /// 数据库字段的用途。
        /// </summary>
        public enum EnumDBFieldUsage
        {
            /// <summary>
            /// 未定义。
            /// </summary>
            None = 0x00,
            /// <summary>
            /// 用于主键。
            /// </summary>
            PrimaryKey = 0x01,
            /// <summary>
            /// 用于唯一键。
            /// </summary>
            UniqueKey = 0x02,
            /// <summary>
            /// 由系统控制该字段的值。
            /// </summary>
            BySystem = 0x04
        }

        [AttributeUsage(AttributeTargets.Property, Inherited = true)]
        public class DBFieldAttribute:Attribute
        {
            EnumDBFieldUsage m_usage;
            string m_strFieldName;
            string m_strDescription;
            object m_defaultValue;

            public DBFieldAttribute(string strFieldName,object defaultValue,EnumDBFieldUsage usage,string strDescription)
            {
                m_strFieldName = strFieldName;
                m_defaultValue = defaultValue;
                m_usage = usage;
                m_strDescription = strDescription;
            }

            public DBFieldAttribute(string fieldName) : this(fieldName,null, EnumDBFieldUsage.None,null)
            { }

            public DBFieldAttribute(string fieldName, EnumDBFieldUsage usage) : this(fieldName, null,usage, null)
            { }

            
            // 获取该成员映射的数据库字段名称。
            public string FieldName
            {
                get
                {
                    return m_strFieldName;
                }
                set
                {
                    m_strFieldName = value;
                }
            }

            // 获取该字段的默认值
            public object DefaultValue
            {
                get
                {
                    return m_defaultValue;
                }
                set 
                {
                    m_defaultValue = value;
                }
            }
        }
    此代码说明了如何制作自定义属性类。其实跟一般的类的区别就是此类继承自Attribute,加上AttributeUsage是属性上的属性,是可选的。

    数据访问层实体类:

    class DalObj
        {
            string m_strTableName;
            int m_nID;
            string m_strName;
            string m_password;

            public DalObj(string strTableName)
            {
                m_strTableName = strTableName;
            }

            [DBField("id",EnumDBFieldUsage.PrimaryKey)]
            public int ID
            {
                get { return m_nID; }
                set { m_nID = value; }
            }

            [DBField("name",DefaultValue="游客")]
            public string Name
            {
                get { return m_strName; }
                set { m_strName = value; }
            }

            [DBField("pwd")]
            public string PassWord
            {
                get { return m_password; }
                set { m_password = value; }
            }
        }
    此代码说明了如何使用自定义的属性。有两点需要注意的地方

    第一:类名可以跟自定义的类名一样,也可以加上或减去后面的Attribute,本例子中就是使用的时候跟自定义的类名减少了“Attribute”。

    第二:属性参数填写方法,如果自定义属性类(例子中DBFieldAttribute)自己的构造函数带参数,那么这些参数是必选的,可以重载构造函数以满足不同组合,必选参数填完之后,可以继续给自定义属性类中的公共成员带命名地赋值,如例子中的 DefaultValue="游客" 一句就是命名参数。

    遍历自定义属性的代码:

                DalObj dalObj = new DalObj("users");
                StringBuilder sb = new StringBuilder();
                foreach (PropertyInfo proInfo in dalObj.GetType().GetProperties())
                {
                    object[] attrs = proInfo.GetCustomAttributes(typeof(DBFieldAttribute), true);
                  if (attrs.Length == 1)
                  {
                      DBFieldAttribute attr = (DBFieldAttribute)attrs[0];
                      sb.Append(attr.FieldName + ":" + (attr.DefaultValue == null ? "null" : attr.DefaultValue.ToString()) + " ");
                  }
                }
                MessageBox.Show(sb.ToString());
    此代码说明了如何检索自定义属性的值,主要用到了GetCustomAttributes来获取属性值。

    四、多线程

    1.对于Thread操作的异常处理

    public static void Main()
    {
      try
      {

      Thread th = new Thread(DoWork);
          th.Start();
      }
      catch (Exception ex)
      {
        // Non-reachable code
        Console.WriteLine ("Exception!");
      }
    }
    static void DoWork()

    {

      ……

       throw null;  // Throws a NullReferenceException

    }   

    在DoWork函数里抛出的异常时不会被主线程的try,catch捕捉的,各个线程应该有自己的try,catch去处理线程异常。

    正确写法:

    public static void Main()
    {
         Thread th = new Thread(DoWork);
          th.Start();
    }
    static void DoWork()
    {
      try
      {
        ……
        throw null;    // The NullReferenceException will be caught below
      }
      catch (Exception ex)
      {
        Typically log the exception, and/or signal another thread
        that we've come unstuck
        ...
      }

    }

    2. 异步函数的异常处理

    例如如 WebClient中的 UploadStringAsync,它的异常会在UploadStringCompleted的参数error里

            static void Main(string[] args)
            {
                WebClient webClient = new WebClient();
                webClient.UploadStringCompleted += new UploadStringCompletedEventHandler((sender, e) =>
                    {
                        if (e.Error != null)
                        {
                            Console.WriteLine(e.Error.Message);
                        }
                    });
                webClient.UploadStringAsync(new Uri("http://www.baidu.com"), "1111");
                Console.ReadKey();
            }

    3 Task的异常处理

    把try包含task.wait()函数,就能捕捉task的异常

    Task task = Task.Run (() => { throw null; });
    try
    {
      task.Wait();
    }
    catch (AggregateException aex)
    {
      if (aex.InnerException is NullReferenceException)
        Console.WriteLine ("Null!");
      else
        throw;
    }

    或者 在continue函数里处理TaskContinuationOptions.OnlyOnFaulted的状态

       Task<List<int>> taskWithFactoryAndState =
       Task.Factory.StartNew<List<int>>((stateObj) =>
           {
               List<int> ints = new List<int>();
                for (int i = 0; i < (int)stateObj; i++)
                 {
                    ints.Add(i);
                    if (i > 100)
                    {
                        InvalidOperationException ex =
                           new InvalidOperationException("oh no its > 100");
                         ex.Source = "taskWithFactoryAndState";
                         throw ex;
                     }
                  }
                return ints;
          }, 2000);


      //and setup a continuation for it only on when faulted
      taskWithFactoryAndState.ContinueWith((ant) =>
         {
            AggregateException aggEx = ant.Exception;
            Console.WriteLine("OOOOPS : The Task exited with Exception(s)");
         foreach (Exception ex in aggEx.InnerExceptions)
            {
                Console.WriteLine(string.Format("Caught exception '{0}'",
                  ex.Message));
            }
         }, TaskContinuationOptions.OnlyOnFaulted);

     //and setup a continuation for it only on ran to completion
      taskWithFactoryAndState.ContinueWith((ant) =>
       {
          List<int> result = ant.Result;
          foreach (int resultValue in result)
          {
             Console.WriteLine("Task produced {0}", resultValue);
          }
       }, TaskContinuationOptions.OnlyOnRanToCompletion);
       Console.ReadLine();

    4 c# 5.0 中的async ,await 异常捕捉

    static async Task ThrowAfter(int timeout, Exception ex)
    {
      await Task.Delay(timeout);
      throw ex;
    }


    static async Task MissHandling()
    {
      var t1 = ThrowAfter(1000, new NotSupportedException("Error 1"));

      try
      {
        await t1;
      }
      catch (NotSupportedException ex)
      {
        Console.WriteLine(ex.Message);
      }
    }

    五、线程同步

    原文地址:http://www.cnblogs.com/baoconghui/archive/2013/05/18/3085253.html

    CSharp中的多线程——线程同步基础

     

    一、同步要领

    1.阻止 (Blocking)

    当一个简易阻止方法、锁系统、信号系统等方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程立刻放弃它被分配的CPU时间,将 它的ThreadState属性添加为WaitSleepJoin状态,不在安排时间直到停止阻止。停止阻止在任意四种情况下发生(关掉电 脑的电源可不算!):

    •   阻止的条件已得到满足
    •   操作超时(如果timeout被指定了)
    •   通过Thread.Interrupt中断了 
    •   通过Thread.Abort放弃了

    当线程通过(不建议)Suspend 方法暂停,不认为是被阻止了。

    2.休眠 和 轮询

    调用Thread.Sleep阻止当前的线程指定的时间(或者直到中断):

    复制代码
    static void Main() {    
    Thread.Sleep (0);    // 释放CPU时间片
    Thread.Sleep (1000);    // 休眠1000毫秒
    Thread.Sleep (TimeSpan.FromHours (1));    // 休眠1小时
    Thread.Sleep (Timeout.Infinite);    // 休眠直到中断
    }
    复制代码

    更确切地说,Thread.Sleep放弃了占用CPU,请求不在被分配时间直到给定的时间经过。Thread.Sleep(0)放弃CPU的时间 刚刚够其它在时间片队列里的活动线程(如果有的话)被执行。
    线程类同时也提供了一个SpinWait方法,它使用轮询CPU而非放弃CPU时间的方式,保持给定的迭代次数进行“无用地繁 忙”。
    一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中 断(Interrupt)。
    SpinWait很少被使用,它的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而 不用调用Sleep方法阻止线程而浪费CPU时间。不过,这种技术的优势只有在多处理器计算机:对单一处理器的电脑,直到轮 询的线程结束了它的时间片之前,一个资源没有机会改变状态,这有违它的初衷。并且调用SpinWait经常会花费较长的时间这 本身就浪费了CPU时间。

    3.阻止vs.轮询

    线程可以等待某个确定的条件来明确轮询,比如 while (!proceed); 或 while (DateTime.Now < nextStartTime);这是非常浪费CPU时间的:对于CLR和操作系统而言,线程进行了一个重要的计算,所以分配了相应的资源!在这种状态下的 轮询线程不算是阻止,不像一个线程等待一个EventWaitHandle(一般使用这样的信号任务来构建)。

    阻止和轮询组合使用可以产生一些变换: while (!proceed) Thread.Sleep (x); // "轮询休眠!" x越大,CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。

    除了稍有延迟,这种轮询和休眠的方式可以结合的非常好。

    4.使用Join等待一个线程完成

    可以通过Join方法阻止线程直到另一个线程结束:

    复制代码
    class JoinDemo {
    
    static void Main() {
    
    Thread t = new Thread (delegate() { Console.ReadLine(); }); 
    t.Start();
    
    t.Join();    // 等待直到线程完成
    
    Console.WriteLine ("Thread t's ReadLine complete!");
    
    }
    
    }
    复制代码

    Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,如果线程已终止,则返回true 。

    二、锁和线程安全

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

    复制代码
    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;
    
    }
    
    }
    
    }
    复制代码

    一个等候竞争锁的线程被阻止时,其ThreadState 状态值为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也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。

    选择同步对象

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

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

    一个专门字段是常用的(如在先前的例子中的locker) , 因为它可以精确控制锁的范围和粒度。
    锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x) 而被 阻止,两者都要调用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) {
    
    ...
    
    }
    
    //释放了锁?没有完全释放!
    
    }
    复制代码

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

    何时进行锁定

    作为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。甚至是极平常的事情——单一字段的赋值操作,都必须考虑到同步问题。

    锁和原子操作

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

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

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

    性能考量

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

    如果耗尽并发,锁定会带来反作用,死锁和争用锁,耗尽并发由于太多的代码被放置到锁语句中了,引起其它线程不必要的被 阻止。死锁是两线程彼此等待被锁定的内容,导致两者都无法继续下去。争用锁是两个线程任一个都可以锁定某个内容,如 果“错误”的线程获取了锁,则导致程序错误。

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

    线程安全

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

    线程安全与.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的话,会抛出异常。胜于直接锁定枚举过程,在这个例子 中,我们首先将项目复制到数组当中,这就避免了固定住锁因为我们在枚举过程中有潜在的耗时。.NET framework一个普遍模式——静态成员是线程 安全的,而一个实例成员则不是。

    三、Interrupt 和 Abort

    一个被阻止的线程可以通过两种方式被提前释放:

      通过 Thread.Interrupt    

      通过 Thread.Abort

    这必须通过另外活动的线程实现,等待的线程是没有能力对它的被阻止状态做任何事情的。

    1.Interrupt方法

    在一个被阻止的线程上调用Interrupt 方法,将强迫释放它,抛出ThreadInterruptedException异常,如下:

    复制代码
    class Program { static void Main() {
    
    Thread t = new Thread (delegate() { try {
    
    Thread.Sleep (Timeout.Infinite);
    
    }
    
    catch (ThreadInterruptedException) { Console.Write ("Forcibly ");
    
    }
    
    Console.WriteLine ("Woken!"); });
    
    t.Start();
     
    
    t.Interrupt();
    
    }
    
    }
    复制代码

    中断一个线程仅仅释放它的当前的(或下一个)等待状态:它并不结束这个线程(当然,除非未处 理ThreadInterruptedException异常)。

    如果Interrupt被一个未阻止的线程调用,那么线程将继续执行直到下一次被阻止时,它抛 出ThreadInterruptedException异常。用下面的测试避免这个问题:

    if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
    
    worker.Interrupt();

    这不是一个线程安全的方式,因为在if语句和worker.Interrupt间可能被抢占了。

    2.Abort方法

    被阻止的线程也可以通过Abort方法被强制释放,这与调用Interrupt相似,除了用ThreadAbortException异常代替 了ThreadInterruptedException异常,此外,异常将在catch里被重新抛出(在试图以有好方式处理异常的时候),直 到Thread.ResetAbort在catch中被调用;在这期间线程的ThreadState为AbortRequested。

    在Interrupt 与 Abort 之间最大不同在于它们调用一个非阻止线程所发生的事情。Interrupt继续工作直到下一次阻止发生,Abort在线程当前所执行的位置(可能甚至不在你的代码中)抛出异常。

    四、线程状态

    你可以通过ThreadState属性获取线程的执行状态。图1将ThreadState列举为“层”。ThreadState被设计的很恐怖,它以 按位计算的方式组合三种状态“层”,每种状态层的成员它们间都是互斥的,下面是所有的三种状态“层”:

    • 运行 (running) / 阻止 (blocking) / 终止 (aborting) 状态(图1显示)
    • 后台 (background) / 前台 (foreground) 状态 (ThreadState.Background)
    • 不建议使用的Suspend 方法(ThreadState.SuspendRequested 和 ThreadState.Suspended)挂起的过程

    总的来说,ThreadState是按位组合零或每个状态层的成员!一个简单的ThreadState例子:

    Unstarted
     
    Running
     
    WaitSleepJoin
     
    Background, Unstarted
     
    SuspendRequested, Background, WaitSleepJoin

    (所枚举的成员有两个从来没被用过,至少是当前CLR实现上:StopRequested 和 Aborted。)

    还有更加复杂的,ThreadState.Running潜在的值为0 ,因此下面的测试不工作:

    if ((t.ThreadState & ThreadState.Running) > 0) ...

    你必须用按位与非操作符来代替,或者使用线程的IsAlive属性。但是IsAlive可能不是你想要的,它在被阻止或挂起的时候返 回true(只有在线程未开始或已结束时它才为false)。

    假设你避开不推荐使用的Suspend 和 Resume方法,你可以写一个helper方法除去所有除了第一种状态层的成员,允许简单 测试计算完成。线程的后台状态可以通过IsBackground 独立地获得,所以实际上只有第一种状态层拥有有用的信息。

    复制代码
    public static ThreadState SimpleThreadState (ThreadState ts)
    
    {
    
    return ts & (ThreadState.Aborted | ThreadState.AbortRequested |
     
    ThreadState.Stopped | ThreadState.Unstarted |
    
    ThreadState.WaitSleepJoin);
    
    }
    复制代码

    ThreadState对调试或程序概要分析是无价之宝,与之不相称的是多线程的协同工作,因为没有一个机制存在:通过判 断ThreadState来执行信息,而不考虑ThreadState期间的变化。

    五、等待句柄

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

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

    EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。这两个类都派生自它们的基类:它们仅有的不同是它们用不同的参数调用基类的构造函数。

    性能方面,使用Wait Handles系统开销会花费在较小(微秒间),不会在它们使用的上下文中产生什么后果。

    AutoResetEvent

    AutoResetEvent就像一个用票通过的旋转门:插入一张票,让正确的人通过。类名字里的“auto”实际上就是旋转门自动关闭 或“重新安排”后来的人让其通过。一个线程等待或阻止通过在门上调用WaitOne方法(直到等到这个“one”,门才开) ,票的 插入则由调用Set方法。如果由许多线程调用WaitOne,在门前便形成了队列,一张票可能来自任意某个线程——换言之,任 何(非阻止)线程要通过AutoResetEvent对象调用Set方法来释放一个被阻止的的线程。

    如果Set调用时没有任何线程处于等待状态,那么句柄保持打开直到某个线程调用了WaitOne 。但是在没人 等的时候重复地在门上调用Set方法不会允许在一队人都通过,在他们到达的时候:仅有下一个人可以通过,多余的票都被“浪费了"。

    WaitOne接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,为了避免过多的阻止发生,WaitOne在等待整段时间里也可以通知离开当前的同步内容。

    Reset方法提供在没有任何等待或阻止的时候关闭旋转门。

    AutoResetEvent可以通过2种方式创建,第一种是通过构造函数:

    EventWaitHandle wh = new AutoResetEvent (false);

    如果布尔参数为真,Set方法在构造后立刻被自动的调用,另一个方法是通过它的基类EventWaitHandle:

    EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);

    EventWaitHandle的构造器也允许创建ManualResetEvent(用EventResetMode.Manual定义). 在Wait Handle不在需要时候,你应当调用Close方法来释放操作系统资源。但是,如果一个Wait Handle将被用于程序(就像这一节的大多例子一样)的生命周期中,我们就可以省略这个步骤,它将在程序域销毁时自动的被销毁。

    接下来这个例子,一个线程开始等待直到另一个线程发出信号。

    复制代码
    class BasicWaitHandle {
    
    static EventWaitHandle wh = new AutoResetEvent (false);
    
    
    static void Main() {
    
    new Thread (Waiter).Start();
    
    Thread.Sleep (1000);    // 等一会...
    
    wh.Set();    // OK ——唤醒它
    
    }
    
    static void Waiter() {
    
    Console.WriteLine ("Waiting...");
    
    wh.WaitOne();    // 等待通知
    
    Console.WriteLine ("Notified");
    
    }
    
    }
    复制代码

    执行过程为:先输入Waiting... 过1妙钟后,输出Notified.

    创建跨进程的EventWaitHandle

    EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的,看这个例子:

    EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto,"MyCompany.MyApp.SomeName");

    如果有两个程序都运行这段代码,他们将彼此可以发送信号,等待句柄可以跨这两个进程中的所有线程。

    任务确认

    设想我们希望在后台完成任务,不在每次我们得到任务时再创建一个新的线程。我们可以通过一个轮询的线程来完成:等待一 个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列 化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。

    我们必须决定要做什么,但是,如果当新的任务来到的时候,工作线程已经在忙之前的任务了,设想这种情形下我们需选择阻 止调用者直到之前的任务被完成。像这样的系统可以用两个AutoResetEvent对象实现:一个“ready”AutoResetEvent, 当准备好的时候,它被工作线程调用Set方法;和“go”AutoResetEvent,当有新任务的时候,它被调用线程调用Set方法。 在下面的例子中,一个简单的string字段被用于决定任务(使用了volatile 关键字声明,来确保两个线程都可以看到相同版 本):

    复制代码
    class AcknowledgedWaitHandle {
    
    static EventWaitHandle ready = new AutoResetEvent (false); static EventWaitHandle go = new AutoResetEvent (false); static volatile string task;
    
    static void Main() {
    
    new Thread (Work).Start();
    // 给工作线程发5次信号
    
    for (int i = 1; i <= 5; i++) {
    
    ready.WaitOne();    // 首先等待,直到工作线程准备好了
    
    task = "a".PadRight (i, 'h');   // 给任务赋值
    
    go.Set();    // 告诉工作线程开始执行!
    
    }
    
    
    // 告诉工作线程用一个null任务来结束 ready.WaitOne(); task = null; go.Set();
    
    }
    
    
    static void Work() {
    
    while (true) {
    
    ready.Set();    // 指明我们已经准备好了
    
    go.WaitOne();    // 等待被踢脱...
    
    if (task == null) return;    // 优雅地退出
    
    Console.WriteLine (task);
    
    }
    
    }
    
    }
    复制代码

    输出结果:

    ah
    ahh
    ahhh
    ahhhh

    注意我们要给task赋null来告诉工作线程退出。倘若我们先调用ready.WaitOne的话,在工作线程上调用Interrupt 或Abort 效果是一样的。因为在调用ready.WaitOne后我们就知道工作线程的确切位置,避免了中断任意代码的复杂性。调用Interrupt 或 Abort需要我们在工作线程中捕捉异常。

    生产者/消费者队列

    另一个普遍的线程方案是在后台工作进程从队列中分配任务。这叫做生产者/消费者队列:在工作线程中生产者入列任务,消费 者出列任务。在下面例子里,一个单独的AutoResetEvent被用于通知工作线程,它只有在用完任务时(队列为空)等待。一个通用的集 合类被用于队列,必须通过锁控制它的访问以确保线程安全。工作线程在队列为null任务时结束:

    复制代码
    using System;
    
    using System.Threading;
    
    using System.Collections.Generic;
    
    
    class ProducerConsumerQueue : IDisposable { EventWaitHandle wh = new AutoResetEvent (false);
    Thread worker;
    
    object locker = new object();
    
    Queue<string> tasks = new Queue<string>();
    
    
    public ProducerConsumerQueue() { worker = new Thread (Work); worker.Start();
    
    }
    
    
    
    public void EnqueueTask (string task) { lock (locker) tasks.Enqueue (task); wh.Set();
    
    }
    
    
    public void Dispose() {
    
    EnqueueTask (null);    // 告诉消费者退出
    
    worker.Join();    // 等待消费者线程完成
    
    wh.Close();    // 释放任何OS资源
    
    }
    
    
    void Work() {
    
    while (true) {
    
    string task = null;
    
    lock (locker)
    
    if (tasks.Count > 0) {
    
    task = tasks.Dequeue(); if (task == null) return;
    
    }
    
    if (task != null) {
    
    Console.WriteLine ("Performing task: " + task);
    
    Thread.Sleep (1000);  // 模拟工作...
    
    }
    
    else
    
    wh.WaitOne();    // 没有任务了——等待信号
    
    }
    
    }
    
    }
    复制代码

    下面是一个主方法测试这个队列:

    复制代码
    class Test {
    
    static void Main() {
    
    using (ProducerConsumerQueue q = new ProducerConsumerQueue()) { q.EnqueueTask ("Hello");
    
    for (int i = 0; i < 10; i++) q.EnqueueTask ("Say " + i);
    q.EnqueueTask ("Goodbye!");
    
    }
    
    //    使用using语句的调用q的Dispose方法, 
    
    //    它入列一个null任务,并等待消费者完成 
    
    }
    
    }
    复制代码

    执行结果:
    Performing task: Hello
    Performing task: Say 1
    Performing task: Say 2
    Performing task: Say 3
    ...
    ...
    Performing task: Say 9
    Goodbye!
    注意我们明确的关闭了Wait Handle在ProducerConsumerQueue被销毁的时候,因为在程序的生命周期中我们可能潜在地 创建和销毁许多这个类的实例。

    ManualResetEvent

    ManualResetEvent是AutoResetEvent变化的一种形式,它的不同之处在于:在线程被WaitOne的调用而通过的时 候,它不会自动地reset,这个过程就像大门一样——调用Set打开门,允许任何数量的已执行WaitOne的线程通过;调 用Reset关闭大门,可能会引起一系列的“等待者”直到下次门打开。

    你可以用一个布尔字段"gateOpen" (用 volatile 关键字来声明)与"spin-sleeping" – 方式结合——重复地检查标志,然后让 线程休眠一段时间的方式,来模拟这个过程。

    ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。

    互斥(Mutex)

    Mutex提供了与C#的lock语句同样的功能,这使它大多时候变得的冗余了。它的优势在于它可以跨进程工作——提供了一计算机范围的锁而胜于程序范围的锁。

    对于一个Mutex类,WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放,就 像C#的lock语句一样,Mutex只能从获取互斥锁的这个线程上被释放。

    Mutex在跨进程的普遍用处是确保在同一时刻只有一个程序的的实例在运行,下面演示如何使用:

    复制代码
    class OneAtATimePlease {
    
    // 使用一个应用程序的唯一的名称(比如包括你公司的URL)
    
    static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo");
    
    
    static void Main() {
    
    //进程中的的另一个实例关闭之后等待5秒如果存在竞争——存在程序在
    if (!mutex.WaitOne (TimeSpan.FromSeconds (5), false)) { Console.WriteLine ("Another instance of the app is running. Bye!"); return;
    
    }
    
    try {
    
    Console.WriteLine ("Running - press Enter to exit");
    
    Console.ReadLine();
    
    }
    
    finally { mutex.ReleaseMutex(); }
    
    }
    
    }
    复制代码

    Mutex有个好的特性是,如果程序结束时而互斥锁没通过ReleaseMutex首先被释放,CLR将自动地释放Mutex。

    Semaphore

    Semaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人可以再进入这个夜总会,并且在 其外会形成一个队列。然后,当人一个人离开时,队列头的人便可以进入了。构造器需要至少两个参数——夜总会的活动的空 间,和夜总会的容量。

    Semaphore 的特性与Mutex 和 lock有点类似,除了Semaphore没有“所有者”——它是不可知线程的,任何 在Semaphore内的线程都可以调用Release,而Mutex 和 lock仅有那些获取了资源的线程才可以释放它。

    复制代码
    class SemaphoreTest {
    
    static Semaphore s = new Semaphore (3, 3);  // Available=3; Capacity=3
    
    
    static void Main() {
    
    for (int i = 0; i < 10; i++) new Thread (Go).Start();
    
    }
    
    
    static void Go() { while (true) { s.WaitOne();
    
    Thread.Sleep (100); // 每次只有3个线程可以到达这里 s.Release();
    
    }
    
    }
    
    }
    复制代码

    WaitAny, WaitAll 和 SignalAndWait

    除了Set 和 WaitOne方法外,在类WaitHandle中还有一些用来创建复杂的同步过程的静态方法。

    WaitAny, WaitAll 和 SignalAndWait使跨多个可能为不同类型的等待句柄变得容易。 SignalAndWait可能是最有用的了:他在某个WaitHandle上调用WaitOne,并在另一个WaitHandle上自动地调 用Set。 你可以在一对EventWaitHandle上装配两个线程,而让它们在某个时间点“相遇” 第一个线程像这样: WaitHandle.SignalAndWait (wh1, wh2); 同时第二个线程做相反的事情:WaitHandle.SignalAndWait (wh2, wh1);

    WaitHandle.WaitAny等待一组等待句柄任意一个发出信号,WaitHandle.WaitAll等待所有给定的句柄发出信号。与票据 旋转门的例子类似,这些方法可能同时地等待所有的旋转门——通过在第一个打开的时候(WaitAny情况下),或者等待直到 它们所有的都打开(WaitAll情况下)。

    六、同步环境

    与手工的锁定相比,你可以进行说明性的锁定,用衍生自ContextBoundObject 并标以Synchronization特性的类,它告 诉CLR自动执行锁操作,看这个例子:

    复制代码
    using System;
    
    using System.Threading;
    
    using System.Runtime.Remoting.Contexts;
    
    
    [Synchronization]
    
    public class AutoLock : ContextBoundObject {
    
    public void Demo() {
    
    Console.Write ("Start...");
    
    Thread.Sleep (1000);    // 我们不能抢占到这
    
    Console.WriteLine ("end");    // 感谢自动锁!
    
    }
    
    }
    
    
    public class Test {
    
    public static void Main() {
    
    AutoLock safeInstance = new AutoLock();
    
    new Thread (safeInstance.Demo).Start();    // 并发地
    
    new Thread (safeInstance.Demo).Start();    // 调用Demo
    
    safeInstance.Demo();    // 方法3次
    
    }
    
    }
    复制代码

    CLR确保了同一时刻只有一个线程可以执行 safeInstance中的代码。它创建了一个同步对象来完成工作,并在每次调 用safeInstance的方法和属性时在其周围只能够行锁定。锁的作用域——这里是safeInstance对象,被称为同步环境。

    那么,它是如何工作的呢?Synchronization特性的命名空间:System.Runtime.Remoting.Contexts是一个线 索。ContextBoundObject可以被认为是一个“远程”对象,这意味着所有方法的调用是被监听的。CLR自动的返回了一个具有相同方法和属性的AutoLock对象的代理对象,它扮演着一个中间者的 角色,这让监听成为可能。监听在每个方法调用时增加了数微秒的时间。

    自动同步不能用于静态类型的成员,和非继承自 ContextBoundObject(例如:Windows Form)的类。

    如果AutoLock是一个集合类,比如说,我们仍然需要一个像下面一样的锁,假设 运行在另一个类里:

    if (safeInstance.Count > 0) safeInstance.RemoveAt (0);

    除非使用这代码的类本身是一个同步的ContextBoundObject!

    同步环境可以扩展到超过一个单独对象的区域。默认地,如果一个同步对象被实例化从在另一段代码之内,它们拥有共享相同 的同步环境(换言之,一个大锁!)。这个行为可以由改变Synchronization特性的构造器的参数来指定。使 用SynchronizationAttribute类定义的常量之一:

    所以如果SynchronizedA的实例被实例化于SynchronizedB的对象中,如果SynchronizedB像下面这样声明的话,它们将有分离 的同步环境:

    [Synchronization (SynchronizationAttribute.REQUIRES_NEW)]

    public class SynchronizedB : ContextBoundObject { ...

    越大的同步环境越容易管理,但是减少机会对有用的并发。 分离的同步环境会造成死锁,看这个例子:

    复制代码
    [Synchronization]
    
    public class Deadlock : ContextBoundObject {
    
    public DeadLock Other;
    
    public void Demo() { Thread.Sleep (1000); Other.Hello(); }
    
    void Hello()    { Console.WriteLine ("hello");    }
    
    }
    
    
    public class Test {
    
    static void Main() {
    
    Deadlock dead1 = new Deadlock(); Deadlock dead2 = new Deadlock(); dead1.Other = dead2;
    
    dead2.Other = dead1;
    
    new Thread (dead1.Demo).Start(); dead2.Demo();
    
    }
    
    }
    复制代码

    为每个Deadlock的实例在Test内创建——一个非同步类,每个实例将有它自己的同步环境,因此,有它自己的锁。当它们 彼此调用的时候,不会花太多时间就会死锁(确切的说是一秒!)。在死锁显而易见的情况下,这与使用明确的锁的方式形成鲜明的对比。

    可重入性问题

    线程安全方法有时候也被称为可重入式的,因为在它执行的时候可以被抢占部分线路,在另外的线程调用也不会带来坏效果。 从某个意义上讲,术语线程安全 和 可重入式的是同义的或者是贴义的。

    不过在自动锁方式上,如果Synchronization的参数可重入式的 为true的话,可重入性会有潜在的问题:[Synchronization(true)] 同步环境的锁在执行离开上下文时被临时地释放。在之前的例子里,这将能预防死锁的发生;很明显很需要这样的功能。然而 一个副作用是,在这期间,任何线程都可以自由的调用在目标对象(“重进入”的同步上下文)的上任何方法,而非常复杂的多线 程中试图避免不释放资源是排在首位的。这就是可重入性的问题。 虽然可重入性是危险的,但有些时候它是不错的选择。比如:设想一个在其内部实现多线程同步的类,将逻辑工作线程运行在 不同的语境中。在没有可重入性问题的情况下,工作线程在它们彼此之间或目标对象之间可能被无理地阻碍。 超过适用的大范围的锁定带来了其它情况没有带来的巨大麻烦——死锁,可重入性问题和被阉割的并发,使另一个更简单的方案——手动的锁定变得更为合适。

  • 相关阅读:
    ARM板卡ftp客户端应用
    vsftp移植(待续)
    /dev/null脚本中作用
    amazeui.css
    将td中文字过长的部分变成省略号显示的小技巧
    div非弹出框半透明遮罩实现全屏幕遮盖css实现
    重新定位svn地址的方法(windows和linux),svn switch(sw)的帮助信息
    linux 下启动SVN服务
    用SVN checkout源码时,设置账号
    svn提示out of date
  • 原文地址:https://www.cnblogs.com/cheng2015/p/4978254.html
Copyright © 2020-2023  润新知