建議22:確保集合的線程安全
集合線程安全是指多個線程上添加或刪除元素時,線程鍵必須保持同步。
下面代碼模擬了一個線程在迭代過程中,另一個線程對元素進行了刪除。
class Program { static List<Person> list = new List<Person>() { new Person() { Name = "Rose", Age = 19 }, new Person() { Name = "Steve", Age = 45 }, new Person() { Name = "Jessica", Age = 20 }, }; static AutoResetEvent autoSet = new AutoResetEvent(false); static void Main(string[] args) { Thread t1 = new Thread(() => { //確保等待t2開始之後才運行下面的代碼 autoSet.WaitOne(); foreach (var item in list) { Console.WriteLine("t1:" + item.Name); Thread.Sleep(1000); } }); t1.Start(); Thread t2 = new Thread(() => { //通知t1可以執行代碼 autoSet.Set(); //沉睡1秒是為了確保刪除操作在t1的迭代過程中 Thread.Sleep(1000); list.RemoveAt(2); }); t2.Start(); } } class Person { public string Name { get; set; } public int Age { get; set; } }
以上代碼運行過程會拋出InvalidOperationException:“集合已修改,可能無法執行枚舉。”
早在泛型集合出現之前,非泛型集合一般提供一個SyncRoot屬性,要保證非泛型集合的線程安全,可以通過鎖定該屬性來實現。如果上面的集合用ArrayList代替,保證其線程安全則應該在迭代和刪除的時候都加上lock,代碼如下:
static ArrayList list = new ArrayList() { new Person() { Name = "Rose", Age = 19 }, new Person() { Name = "Steve", Age = 45 }, new Person() { Name = "Jessica", Age = 20 }, }; static AutoResetEvent autoSet = new AutoResetEvent(false); static void Main(string[] args) { Thread t1 = new Thread(() => { //確保等待t2開始之後才運行下面的代碼 autoSet.WaitOne(); lock (list.SyncRoot) { foreach (Person item in list) { Console.WriteLine("t1:" + item.Name); Thread.Sleep(1000); } } }); t1.Start(); Thread t2 = new Thread(() => { //通知t1可以執行代碼 autoSet.Set(); //沉睡1秒是為了確保刪除操作在t1的迭代過程中 Thread.Sleep(1000); lock (list.SyncRoot) { list.RemoveAt(2); Console.WriteLine("刪除成功"); } }); t2.Start(); }
以上代碼不會拋出異常,因為鎖定通過互斥的機制保證了同一時刻只能有一個線程操作集合元素。我們進而發現泛型集合沒有這樣的屬性,必須要自己創建一個鎖定對象來完成同步任務。可以通過new一個靜態對象來進行鎖定,代碼如下:
static List<Person> list = new List<Person>() { new Person() { Name = "Rose", Age = 19 }, new Person() { Name = "Steve", Age = 45 }, new Person() { Name = "Jessica", Age = 20 }, }; static AutoResetEvent autoSet = new AutoResetEvent(false); static object sycObj = new object(); static void Main(string[] args) { //object sycObj = new object(); Thread t1 = new Thread(() => { //確保等待t2開始之後才運行下面的代碼 autoSet.WaitOne(); lock (sycObj) { foreach (Person item in list) { Console.WriteLine("t1:" + item.Name); Thread.Sleep(1000); } } }); t1.Start(); Thread t2 = new Thread(() => { //通知t1可以執行代碼 autoSet.Set(); //沉睡1秒是為了確保刪除操作在t1的迭代過程中 Thread.Sleep(1000); lock (sycObj) { list.RemoveAt(2); Console.WriteLine("刪除成功"); } }); t2.Start(); }
轉自:《編寫高質量代碼改善C#程序的157個建議》陸敏技