您的位置:首页 > 其它

多线程操作集合时如何保证集合的线程安全性

2017-03-23 20:05 239 查看
using System;  

using System.Collections.Generic;  

using System.Threading;  

   

namespace CollSecExp  

{  

    class Program  

    {          

        static void Main(string[] args)  

        {  

            List<int> list = new List<int>();  

            for (int i = 0; i < 10; i++)  

            {  

                list.Add(i);  

            }  

  

            Thread t1 = new Thread(() =>  

            {  

                 foreach (var item in list)  

                 {  

                     Console.WriteLine("t1.item:{0}", item);  

                     Thread.Sleep(1000);  

                 }  

            });  

            t1.Start();  

  

            Thread t2 = new Thread(() =>  

            {  

                Thread.Sleep(1000);  

                list.RemoveAt(1);  

                list.RemoveAt(3);  

                foreach (var item in list)  

                {  

                     Console.WriteLine("t2.item:{0}", item);  

                 }  

            });  

            t2.Start();  

        }  

    }  

}  

运行示例代码1,会抛出InvalidOperationException异常,提示“集合已修改;可能无法执行枚举操作。”

这是因为,线程2移除index=1,3的元素导致集合被修改,很显然,此时线程1遍历集合肯定会出错,因为它这时遍历得到的结果与实际情况已经不符,就算取出也没有了价值。

 

针对这种情况,我们可以对集合加锁来保证线程对集合操作的同步。


修改示例代码1,得到示例代码2

[csharp] view
plain copy

using System;  

using System.Collections.Generic;  

using System.Threading;  

   

namespace CollSecExp  

{  

    class Program  

    {          

        static void Main(string[] args)  

        {  

            object sycObj = new object();  

            List<int> list = new List<int>();  

            for (int i = 0; i < 10; i++)  

            {  

                list.Add(i);  

            }  

   

            Thread t1 = new Thread(() =>  

            {  

                lock (sycObj)  

                {  

                    foreach (var item in list)  

                    {  

                        Console.WriteLine("t1.item:{0}", item);  

                        Thread.Sleep(1000);  

                    }  

                }  

            });  

            t1.Start();  

  

            Thread t2 = new Thread(() =>  

            {  

                Thread.Sleep(1000);  

                lock (sycObj)  

                {  

                    list.RemoveAt(1);  

                    list.RemoveAt(3);  

                    foreach (var item in list)  

                    {  

                        Console.WriteLine("t2.item:{0}", item);  

                    }  

               }  

            });  

            t2.Start();  

        }  

    }  

}  

示例代码2与示例代码1的区别仅在加锁操作上,变化的地方已经用红色字体标出。

再次运行代码,未报错,得到以下运行结果:

 


从结果我们可以看出,加锁以后,线程1执行的结果是正常的,正确的输出了list的10个元素。同样,线程2执行结果也是正常的,移除index=1,3的元素后,遍历出剩下的8个元素。不过这里要补充说明一下,移除index=1,3 的元素时,为什么是源集合中的index=1,4的元素被移除?那是因为移除index=1的元素后,index=4的元素在新集合中的index就变成3了,所以会有这样的结果。

 

另外,在编写测试代码时,还遇到了下面这样一个问题,拿出来分享一下。


将线程2的代码改写成下面的形式。

[csharp] view
plain copy

Thread t2 = new Thread(() =>  

{  

     Thread.Sleep(1000);  

     lock (sycObj)  

     {  

          foreach(var item in list)  

          {  

               if (item % 2 == 0)  

               {  

                    list.Remove(item);  

               }  

          }  

          foreach (var item in list)  

          {  

               Console.WriteLine("t2.item:{0}", item);  

          }  

     }  

});  

t2.Start();  

运行程序,你会发现,线程1能正常运行,但是在执行线程2时,同样会抛InvalidOperationException异常,提示信息仍为“集合已修改;可能无法执行枚举操作。”

分析代码,发现是在遍历集合并作移除操作的代码部分(已用青色字体标出),对于这点其实不在多线程程序中也是会出错的。很明显这种做法就不对,你既想遍历集合又想动态删除某些符合条件的元素,这是不可能的,哪有这么好的事。

 

其实,除了加锁,还可以使用System.Collections.Concurrent命名空间中提供的ConcurrentBag<T>来代替List<T>,ConcurrentBag<T>是线程安全的集合类。


代码片段3:

[csharp] view
plain copy

using System;  

using System.Collections.Generic;  

using System.Threading;  

using System.Collections.Concurrent;   //注1   

  

namespace CollSecExp  

{  

    class Program  

    {          

        static void Main(string[] args)  

        {  

            ConcurrentBag<int> list = new ConcurrentBag<int>();  

            for (int i = 0; i < 10; i++)  

            {  

                list.Add(i); //注2  

            }  

   

            Thread t1 = new Thread(() =>  

            {  

                foreach (var item in list)  

                {  

                    Console.WriteLine("t1.item:{0}", item);  

                    Thread.Sleep(1000);  

                }  

            });  

            t1.Start();  

  

            Thread t2 = new Thread(() =>  

            {  

                Thread.Sleep(1000);  

                int a;  

                list.TryTake(out a); //注3  

                foreach (var item in list)  

                {  

                    Console.WriteLine("t2.item:{0}", item);  

                }  

            });  

            t2.Start();  

        }  

    }  

}  

示例代码3与示例代码1的区别在于使用ConcurrentBag<int>来代替List<int>保存集合数据。这样一来就算不加锁,也能保证线程操作的同步。

有几点稍微补充说明一下:

[注1]需要引用命名空间System.Collections.Concurrent,才可以使用ConcurrentBag<T>类。

[注2]ConcurrentBag<T>的Add方法与List<T>的Add方法相同。

[注3]ConcurrentBag<T>的移除方法为TryTake,与List<T>的移除方法不相同,这点需要注意。

 运行程序,得到结果:

 


会发现线程1与线程2能正常运行,只是此时线程1,2会并行执行,且遍历集合时会从项值大的到小的顺序进行,关于这点,以后再研究。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐