在多线程内使用集合,如果未对集合做任何安全处理,就非常容易出现系统崩溃或各种错误。最近的项目里,使用的是socket通信后再改变了某个集合,结果导致系统直接崩溃,且无任何错误系统弹出。
经排查,发现问题是执行某集合后,系统就会在一定时间内退出,最后发现是使用的一个字典集合出了问题。稍微思考后,就认定了是线程安全问题。因为此集合在其它几个地方都有线程做循环读取。
下面是我模拟的一个示例,没有进行任何的安全处理:
1 class Program 2 { 3 static MyCollection mycoll; 4 static void Main(string[] args) 5 { 6 mycoll = new MyCollection(); 7 Thread readT = new Thread(new ThreadStart(ReadMethod)); 8 readT.Start(); 9 10 Thread addT = new Thread(new ThreadStart(AddMethod)); 11 addT.Start(); 12 Console.ReadLine(); 13 } 14 public static void AddMethod() 15 { 16 for(int i=0;i<10;i++) 17 { 18 Thread.Sleep(500); 19 mycoll.Add("a"+i, i); 20 } 21 } 22 public static void ReadMethod() 23 { 24 while (true) 25 { 26 Thread.Sleep(100); 27 foreach (KeyValuePair<string, int> item in mycoll.myDic) 28 { 29 Console.WriteLine(item.Key + "\t" + item.Value); 30 //其它处理 31 Thread.Sleep(2000); 32 } 33 } 34 } 35 } 36 public class MyCollection 37 { 38 public Dictionary<string, int> myDic = new Dictionary<string, int>(); 39 40 public void Add(string key, int value) 41 { 42 if (myDic.ContainsKey(key)) 43 { 44 myDic[key] += 1; 45 } 46 else 47 { 48 myDic.Add(key, value); 49 } 50 } 51 52 public void Remove(string key) 53 { 54 if (myDic.ContainsKey(key)) 55 { 56 myDic.Remove(key); 57 } 58 } 59 }
在上面的示例中,创建了一个Dictionary字典对像,程序运行时,输出了下面的错误:
程序运行时,输出了上面的错误,仅仅输出了一行结果
这次测试有明显示的错误提示,集合已修改;可能无法执行枚举操作。
唉,真是一个常见的问题,在foreach的时侯又修改集合,就一定会出现问题了,因为foreach是只读的,在进行遍历时不可以对集合进行任何修改。
看到这里,我们会想到,如果使用for循环进行逆向获取,也许可以解决此问题。
非常可惜,字典对像没有使用索引号获取的办法,下面的表格转自(http://www.cnblogs.com/yang_sy/p/3678905.html)
Type | 内部结构 | 支持索引 | 内存占用 | 随机插入的速度(毫秒) | 顺序插入的速度(毫秒) | 根据键获取元素的速度(毫秒) |
未排序字典 | ||||||
Dictionary<T,V> | 哈希表 | 否 | 22 | 30 | 30 | 20 |
Hashtable | 哈希表 | 否 | 38 | 50 | 50 | 30 |
ListDictionary | 链表 | 否 | 36 | 50000 | 50000 | 50000 |
OrderedDictionary | 哈希表 +数组 | 是 | 59 | 70 | 70 | 40 |
排序字典 | ||||||
SortedDictionary<K,V> | 红黑树 | 否 | 20 | 130 | 100 | 120 |
SortedList<K,V> | 2xArray | 是 | 20 | 3300 | 30 | 40 |
SortList | 2xArray | 是 | 27 | 4500 | 100 | 180 |
从时间复杂度来讲,从字典中通过键获取值所耗费的时间分别如下:
- Hashtable, Dictionary和OrderedDictionary的时间复杂度为O(1)
- SortedDictionary和SortList的时间复杂度为O(logN)
- ListDictinary的时间复杂度为O(n)
这可如何是好,只能改为可排序的对像?然后使用for解决?
我突然想到,是否可以在循环时缩短foreach,来解决此问题呢?
想到可以在循环时先copy一份副本,然后再进行循环操作,编写代码,查找copy的方法。真是无奈,没有提供任何的copy方法。唉!看来人都是用来被逼的,先改个对象吧:
把Dictionary修改成了Hashtable对像(也没有索引排序)。代码如下:
1 class Program 2 { 3 static MyCollection mycoll; 4 static void Main(string[] args) 5 { 6 mycoll = new MyCollection(); 7 Thread readT = new Thread(new ThreadStart(ReadMethod)); 8 readT.Start(); 9 10 Thread addT = new Thread(new ThreadStart(AddMethod)); 11 addT.Start(); 12 Console.ReadLine(); 13 } 14 public static void AddMethod() 15 { 16 for(int i=0;i<10;i++) 17 { 18 Thread.Sleep(500); 19 mycoll.Add("a"+i, i); 20 } 21 } 22 public static void ReadMethod() 23 { 24 while (true) 25 { 26 Thread.Sleep(100); 27 foreach (DictionaryEntry item in mycoll.myDic) 28 { 29 Console.WriteLine(item.Key + " " + item.Value); 30 //其它处理 31 Thread.Sleep(2000); 32 } 33 } 34 } 35 } 36 public class MyCollection 37 { 38 public Hashtable myDic = new Hashtable(); 39 40 public void Add(string key, int value) 41 { 42 if (myDic.ContainsKey(key)) 43 { 44 45 myDic[key] =Convert.ToInt32(myDic[key])+ 1; 46 } 47 else 48 { 49 myDic.Add(key, value); 50 } 51 } 52 53 public void Remove(string key) 54 { 55 if (myDic.ContainsKey(key)) 56 { 57 myDic.Remove(key); 58 } 59 } 60 }
代码一如即往的报错,错误信息一样。
使用copy法试试
1 class Program 2 { 3 static MyCollection mycoll; 4 static void Main(string[] args) 5 { 6 mycoll = new MyCollection(); 7 Thread readT = new Thread(new ThreadStart(ReadMethod)); 8 readT.Start(); 9 10 Thread addT = new Thread(new ThreadStart(AddMethod)); 11 addT.Start(); 12 Console.ReadLine(); 13 } 14 public static void AddMethod() 15 { 16 for(int i=0;i<10;i++) 17 { 18 Thread.Sleep(500); 19 mycoll.Add("a"+i, i); 20 } 21 } 22 public static void ReadMethod() 23 { 24 Hashtable tempHt = null; 25 while (true) 26 { 27 Thread.Sleep(100); 28 tempHt = mycoll.myDic.Clone() as Hashtable; 29 Console.WriteLine(" ================================= "); 30 foreach (DictionaryEntry item in tempHt) 31 { 32 Console.WriteLine(item.Key + " " + item.Value); 33 //其它处理 34 Thread.Sleep(2000); 35 } 36 } 37 } 38 } 39 public class MyCollection 40 { 41 public Hashtable myDic = new Hashtable(); 42 43 public void Add(string key, int value) 44 { 45 if (myDic.ContainsKey(key)) 46 { 47 48 myDic[key] =Convert.ToInt32(myDic[key])+ 1; 49 } 50 else 51 { 52 myDic.Add(key, value); 53 } 54 } 55 56 public void Remove(string key) 57 { 58 if (myDic.ContainsKey(key)) 59 { 60 myDic.Remove(key); 61 } 62 } 63 }
输出结果如下:
以上结果输出
写到这里,我自己都有些模糊了。这文章和线程安全有毛关系。
根据msdn线程安全解释如下:
Hashtable 是线程安全的,可由多个读取器线程或一个写入线程使用。多线程使用时,如果任何一个线程执行写入(更新)操作,它都不是线程安全的。若要支持多个编写器,如果没有任何线程在读取 Hashtable 对象,则对 Hashtable 的所有操作都必须通过 Synchronized 方法返回的包装完成。
从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。
1 class Program 2 { 3 static MyCollection mycoll; 4 static void Main(string[] args) 5 { 6 mycoll = new MyCollection(); 7 Thread readT = new Thread(new ThreadStart(ReadMethod)); 8 readT.Start(); 9 10 Thread addT = new Thread(new ThreadStart(AddMethod)); 11 addT.Start(); 12 13 14 Thread addT2 = new Thread(new ThreadStart(AddMethod2)); 15 addT2.Start(); 16 17 Thread delT = new Thread(new ThreadStart(DelMethod)); 18 delT.Start(); 19 20 Thread delT2 = new Thread(new ThreadStart(DelMethod2)); 21 delT2.Start(); 22 23 Console.ReadLine(); 24 } 25 26 public static void DelMethod() 27 { 28 for (int i = 0; i < 10; i++) 29 { 30 Thread.Sleep(800); 31 if(mycoll.myDic.ContainsKey("a"+i)) 32 mycoll.myDic.Remove("a" + i); 33 } 34 } 35 36 public static void DelMethod2() 37 { 38 for (int i = 0; i < 10; i++) 39 { 40 Thread.Sleep(800); 41 if (mycoll.myDic.ContainsKey("b" + i)) 42 mycoll.myDic.Remove("b" + i); 43 } 44 } 45 46 public static void AddMethod2() 47 { 48 for (int i = 0; i < 10; i++) 49 { 50 Thread.Sleep(500); 51 mycoll.Add("b" + i, i); 52 } 53 } 54 public static void AddMethod() 55 { 56 for(int i=0;i<10;i++) 57 { 58 Thread.Sleep(500); 59 mycoll.Add("a"+i, i); 60 } 61 } 62 public static void ReadMethod() 63 { 64 Hashtable tempHt = null; 65 while (true) 66 { 67 Thread.Sleep(100); 68 lock (mycoll.myDic.SyncRoot) 69 { 70 tempHt = mycoll.myDic.Clone() as Hashtable; 71 } 72 Console.WriteLine(" ================================= "); 73 foreach (DictionaryEntry item in tempHt) 74 { 75 Console.WriteLine(item.Key + " " + item.Value); 76 //其它处理 77 Thread.Sleep(600); 78 } 79 } 80 } 81 } 82 public class MyCollection 83 { 84 public Hashtable myDic = new Hashtable(); 85 86 public void Add(string key, int value) 87 { 88 lock (myDic.SyncRoot) 89 { 90 if (myDic.ContainsKey(key)) 91 { 92 93 myDic[key] = Convert.ToInt32(myDic[key]) + 1; 94 } 95 else 96 { 97 myDic.Add(key, value); 98 } 99 } 100 } 101 102 public void Remove(string key) 103 { 104 if (myDic.ContainsKey(key)) 105 { 106 lock (myDic.SyncRoot) 107 { 108 myDic.Remove(key); 109 } 110 } 111 } 112 }
时间损耗
1 public static void ReadMethod() 2 { 3 Hashtable tempHt = null; 4 System.Diagnostics.Stopwatch stopwatch = new Stopwatch(); 5 stopwatch.Start(); // 开始监视代码运行时间 6 while (true) 7 { 8 Thread.Sleep(100); 9 lock (mycoll.myDic.SyncRoot) 10 { 11 tempHt = mycoll.myDic.Clone() as Hashtable; 12 } 13 Console.WriteLine(" ================================= "); 14 foreach (DictionaryEntry item in tempHt) 15 { 16 Console.WriteLine(item.Key + " " + item.Value); 17 //其它处理 18 Thread.Sleep(600); 19 } 20 if (tempHt != null && tempHt.Count == 20) 21 { 22 break; 23 } 24 } 25 stopwatch.Stop(); // 停止监视 26 TimeSpan timespan = stopwatch.Elapsed; // 获取当前实例测量得出的总时间 27 Console.WriteLine("全部加满用时:" + timespan.Milliseconds); 28 } 29 }
好了,多线程安全问题就说到这里,总结来说就是注意锁在多线程中的应用。
如有此文章内存在问题,还请多多指正。