建议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个建议》陆敏技