首页 > 代码库 > 多线程下的集合安全

多线程下的集合安全

        在多线程内使用集合,如果未对集合做任何安全处理,就非常容易出现系统崩溃或各种错误。最近的项目里,使用的是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>哈希表22303020
Hashtable哈希表38505030
ListDictionary链表36500005000050000
OrderedDictionary哈希表 +数组59707040
排序字典      
SortedDictionary<K,V>红黑树20130100120
SortedList<K,V>2xArray2033003040
SortList2xArray274500100180

从时间复杂度来讲,从字典中通过键获取值所耗费的时间分别如下:

  • 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 方法返回的包装完成。

从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。

经过我们模拟,没有发现多线程下错误,但为安全起见,我们在使用时,最好根据msdn所述,在对线程操作时加上安全锁处理,这里我们不需自己定义锁对象,因为微软直接提供了SyncRoot进行安全锁处理。
修改后的代码如下:
  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     }

 

好了,多线程安全问题就说到这里,总结来说就是注意锁在多线程中的应用。

如有此文章内存在问题,还请多多指正。

 

 

多线程下的集合安全