多线程下的集合安全

        在多线程内使用集合,如果未对集合做任何安全处理,就非常容易出现系统崩溃或各种错误。最近的项目里,使用的是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("\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 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 方法返回的包装完成。

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

经过我们模拟,没有发现多线程下错误,但为安全起见,我们在使用时,最好根据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     }

 

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

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

 

 

posted @ 2014-09-18 10:32  迟来的春天  阅读(4121)  评论(20编辑  收藏  举报