• 多线程下的集合安全


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

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

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

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

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

  • 相关阅读:
    利用web前端综合制作一个注册功能
    使用 kubeadm 快速部署一个 Kubernetes 集群
    部署一套完整的Kubernetes高可用集群(二进制,最新版v1.18)下
    ! [rejected] master -> master (non-fast-forward) error: failed to push some refs to 'gitee.com...'
    fatal: unable to access 'https://gitee.com/...': Could not resolve host: gitee.com
    Qt学生管理系统
    Qt5.14.2生成qsqlmysql.lib
    express框架实现数据库CRUD操作
    链表常见的题型和解题思路
    2 引用
  • 原文地址:https://www.cnblogs.com/cldct/p/3978686.html
Copyright © 2020-2023  润新知