建议18中提到,foreach循环不能代替for循环的一个原因是在迭代过程中对集合本身进行了增删操作。将此场景移植到多线程场景中,就是本建议要阐述的重点:确保集合的线程安全。集合线程安全是指在多个线程上添加或删除元素时,线程之间必须保持同步。
下面的代码模拟了一个线程在迭代过程中,另外一个线程对集合的元素进行了删除。
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();
}
以上代码不会抛出异常,因为锁定通过互斥的机制保证了同一时刻只能有一个线程操作集合元素。我们进而发现泛型集合没有这样的属性,必须要自己创建一个锁定对象来完成同步任务。在当前的这个例子中,可以通过为Program类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();
}
在“建议21:选择正确的集合”中,还指出了在命名空间System.Collections.Concurrent下有若干实现了线程安全的集合类,在多线程应用环境下,我们可以根据实际需求选择这些集合类型。