首页 > 代码库 > 多线程下的集合安全
多线程下的集合安全
在多线程内使用集合,如果未对集合做任何安全处理,就非常容易出现系统崩溃或各种错误。最近的项目里,使用的是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 MyCollection37 {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 else47 {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 MyCollection37 {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 else48 {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("\r\n=================================\r\n");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 MyCollection40 {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 else51 {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("\r\n=================================\r\n"); 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("\r\n=================================\r\n");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 }
好了,多线程安全问题就说到这里,总结来说就是注意锁在多线程中的应用。
如有此文章内存在问题,还请多多指正。
多线程下的集合安全