C#編程技巧

拷貝/克隆(深度/淺拷貝)、序列化、反射html

Json/Xmlnode

窗體的單例模式web

https://www.cnblogs.com/imstrive/p/5426503.html數據庫

 

使用XmlSerializer序列化可空屬性編程

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

 

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

 

 

1、淺拷貝和深拷貝

有兩種對象克隆的方法:淺拷貝和深拷貝。淺拷貝只是複製引用,而不會複製引用的對象。深拷貝會複製引用的對象。

所以,原始對象中的引用和淺拷貝對象中的同一個引用都指向同一個對象。而深拷貝的對象包含了對象的一切直接或間接的引用。參看維基百科(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)建立了一個自定義的克隆框架。這個框架可以建立對象及其引用的對象的深拷貝,無論對象結構有多複雜。缺點是,這是一個不提供源代碼的自定義 框架(更新:如今已經包括源代碼了,參見本文評論),而且它不能在不使用無參數構造器的時候,拷貝由私有方法建立的對象。另外一個問題,也是全部自動化的深 克隆方法共有的問題是,深拷貝一般須要靈活地處理不能進行簡單自動化特殊狀況(例如未受管理的資源)。

 

2、數據庫操做

一、在使用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")便可

 

3、自定義屬性

描述

屬性提供功能強大的方法以將聲明信息與 C# 代碼(類型、方法、屬性等)相關聯。屬性與程序實體關聯後,便可在運行時使用名爲「反射」的技術查詢屬性。

屬性以兩種形式出現:

  • 一種是在公共語言運行庫 (CLR) 中定義的屬性。

  • 另外一種是能夠建立的用於向代碼中添加附加信息的自定義屬性。此信息可在之後以編程方式檢索。

自定義屬性的做用

      有時候咱們須要給一個類或者類中的成員加上一些屬性或者附加信息,讓類或者變量的功能更明確可控制的細粒度更高,打個簡單的比方:數據庫裏面的一張表,表中的每個字段都有不少屬性,如是否主鍵,默認值,註釋信息等等,咱們在編寫實體類的時候,如何表示這些信息呢?經過自定義屬性能夠實現。

自定義屬性的實現步驟

一、聲明一個類,並將 AttributeUsageAttribute 屬性應用到該類中。類的名稱即爲新屬性的名稱 
二、聲明該類從 System.Attribute 繼承: 
三、定義 Private 字段來存儲屬性值: 
四、須要時,請爲屬性建立構造函數: 
五、爲屬性 (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()) + "\r\n");
              }
            }
            MessageBox.Show(sb.ToString());
此代碼說明了如何檢索自定義屬性的值,主要用到了GetCustomAttributes來獲取屬性值。

 

4、多線程

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);
  }
}

 

5、線程同步

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

CSharp中的多線程——線程同步基礎

 

1、同步要領

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 。

 

2、鎖和線程安全

鎖實現互斥的訪問,被用於確保在同一時刻只有一個線程能夠進入特殊的代碼片斷,考慮下面的類:

複製代碼
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一個廣泛模式——靜態成員是線程 安全的,而一個實例成員則不是。

3、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在線程當前所執行的位置(可能甚至不在你的代碼中)拋出異常。

 

4、線程狀態

你能夠經過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期間的變化。

5、等待句柄

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狀況下)。

 

6、同步環境

與手工的鎖定相比,你能夠進行說明性的鎖定,用衍生自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)] 同步環境的鎖在執行離開上下文時被臨時地釋放。在以前的例子裏,這將能預防死鎖的發生;很明顯很須要這樣的功能。然而 一個反作用是,在這期間,任何線程均可以自由的調用在目標對象(「重進入」的同步上下文)的上任何方法,而很是複雜的多線 程中試圖避免不釋放資源是排在首位的。這就是可重入性的問題。 雖然可重入性是危險的,但有些時候它是不錯的選擇。好比:設想一個在其內部實現多線程同步的類,將邏輯工做線程運行在 不一樣的語境中。在沒有可重入性問題的狀況下,工做線程在它們彼此之間或目標對象之間可能被無理地阻礙。 超過適用的大範圍的鎖定帶來了其它狀況沒有帶來的巨大麻煩——死鎖,可重入性問題和被閹割的併發,使另外一個更簡單的方案——手動的鎖定變得更爲合適。

相關文章
相關標籤/搜索