c# 并发编程系列之四:集合的并发处理

c#中有很多的集合,分别属于两个不同的名称空间:System.Collections 和

System.Collections.Generic,其中 System.Collections.Generic 是泛型集合,

泛型集合可以避免装箱和拆箱操作,有更高的效率,对编程也更友好,这两个

名称空间下的集合分别如下表:

System.Collections 说明
ArrayList 使用大小会根据需要动态增加的数组来实现 IList 接口。
BitArray

管理位值的压缩数组,这些值以布尔值的形式表示,

其中 true 表示此位为开 (1),false 表示此位为关 (0)。

Hashtable

表示根据键的哈希代码进行组织的键/值对的集合。

枚举的泛型结构是 DictionaryEntry。

Queue 表示对象的先进先出集合。
SortedList 表示键/值对的集合,这些键值对按键排序并可按照键和索引访问。
Stack 表示对象的简单后进先出 (LIFO) 非泛型集合。
   

泛型集合如下表:

System.Collections.Generic  
Dictionary<TKey,TValue>

表示键和值的集合,是Hashtable 的泛型版本,

它的枚举的泛型结构是 KeyValuePair<TKey,TValue> 而不是 DictionaryEntry。

HashSet<T> 表示值的集合。
LinkedList<T> 表示双重链接列表。
List<T>

表示可通过索引访问的对象的强类型列表。

提供用于对列表进行搜索、排序和操作的方法,是ArrayList 的泛型版本。

Queue<T> 表示对象的先进先出集合。
SortedDictionary<TKey,TValue> 表示根据键进行排序的键/值对的集合。
SortedList<TKey,TValue> 表示基于相关的 IComparer<T> 实现按键进行排序的键/值对的集合。
SortedSet<T> 表示按排序顺序维护的对象的集合。
Stack<T> 表示相同指定类型的实例可变大小的后进先出 (LIFO) 集合。
   

在大部分情况下,我们都是使用这些集合中完成对元素的增/减、读/写操作,但这些集合不是线程安全的,在并发的时候就可以能出现问题。

考虑到如下一个场景:

有一个在线学习系统,用户学习完一定的课程后需要统一进行考试来检验他们学习的效果,

培训机构定一个考试时间,所有用户在规定的时间内完成相同的试卷。

页面操作:

当用户登录系统后,在考试页面点【我要考试】的按钮,系统随机从题库中抽取100道题,

然后显示到页面上给用户进行考试,页面如下:

当用户点【开始考试】后,在 Exam.cshtml.cs 文件中生成考题的代码如下:

namespace ConcurrencyDemo.Pages
{
    public class ExamModel : PageModel
    {

        public void OnGet()
        {
            CreateQuestions();
        }

        public List<string> QuestionList;
        private void CreateQuestions()
        {
       //为了演示方便,使用 Mock 的方式只取20笔数据,实际项目中需要到数据库中抓取 QuestionList
= new List<string>() { "长期成本可划分为长期可变成本和长期不变成本。", "再贴现率下降表明货币当局试图扩大货币和信贷的供给。", "在经济过热时,政府应该增加税收和减少支出。", "西方经济学一般可分为微观经济学和宏观经济学两部分。", "为了取得最大利润,厂商应使其产品的平均收益等于平均成本。", "在其他条件不变的情况下,某种商品的需求量随着替代品价格的提高而增加。", "累进个人所得税具有内在稳定器功能。", "SMC等于SAC时,SAC达到最低点(对)。", "实行扩张性财政政策有助于抑制通货膨胀。", "计算我国的国内生产总值时,不计入外商独资企业创造的价值。", "某公司是日商独资企业,因此该公司的产值不计入我国的国内生产总值。", "如果GDP大于GNP,说明外国人在该国取得的收入大于该国人在国外取得的收入。", "对于任何类型的市场,P=AR=MR都是成立的。", "只有在资源已经得到充分利用的前提下,乘数效应才能发挥作用。", "在美国,投资是GDP中最大的组成部分。", "如果政府决定使经济摆脱衰退,就应当使用能减少总需求的各项政策工具。", "贫困救济金可以发挥财政政策“内在稳定器”的功能。", "规模收益递减的原因在于边际收益递减规律。", "在垄断竞争市场,每个厂商所面临的需求曲线都时向右下方倾斜的。", "边际产量曲线一定在平均产量曲线的最高点与之相交。", }; } } }

页面 Exam.cshtml 的代码如下:

@page
@model ConcurrencyDemo.Pages.ExamModel
@{
    ViewData["Title"] = "Exam";
}

<ol>
    @foreach (string str in Model.QuestionList)
    {
    <li style="height:30px;line-height:30px;">
        <input type="radio" name="yesorno" value="1"><span style="color:blue;"></span> &nbsp;&nbsp;&nbsp;
        <input type="radio" name="yesorno" value="0"><span style="color:blue;"></span>
        &nbsp;&nbsp;&nbsp;&nbsp;@str
    </li>
    }
</ol>

编译后运行效果如下:

如果开启多个线程给泛型的 List<string>添加元素,情况就不一样了。

将 Exam.cshtml.cs 中代码修改如下:

namespace ConcurrencyDemo.Pages
{
    public class ExamModel : PageModel
    {

        public void OnGet()
        {
            CreateQuestions21();
        }

        public List<string> QuestionList = new List<string>(); 
        private void CreateQuestions21()
        {
            Parallel.For(1, 101, i => //使用多线程循环100次,期望在泛型集合QuestionList中写入100个题目
            {
                Thread.Sleep(90); //模拟一个耗时操作
                QuestionList.Add("题目 " + i.ToString()); //将题目写入到集合中,不同的题目用变量 i 来区分
            });
        }
   }
}

Exam.cshtml 中代码修改如下:

@page
@model ConcurrencyDemo.Pages.ExamModel
@{
    ViewData["Title"] = "Exam";
}

<table border="1">
    <tr>
        @for (int i = 0; i < Model.QuestionList.Count; i++)
        { 
            <td style="height:30px;border:solid 1px #c0c0c0;">
                @(i+1)&nbsp;
                <input type="radio" name="yesorno" value="1"><span style="color:blue;"></span> &nbsp;
                <input type="radio" name="yesorno" value="0"><span style="color:blue;"></span>
                &nbsp;&nbsp;&nbsp;&nbsp; @Model.QuestionList[i]
            </td>

            @if (i % 10 == 9)
            {
                @Html.Raw("</tr><tr>")
            }
        }
    </tr>
</table>

编译后运行,结果如下:

可以看到最大的序号是 91 , 即实际上只写入了 91 个题目信息,如果我们刷新页面的,这个最大序号也会发生变化,如下图 :

此时变成了87,继续刷新页面,这个值会跟着变化,但始终是 < 100的。所以,对于 List<T> 这样的集合来说,在多线程的情况下,

会出现线程安全问题,导致结果不合符我们的预期。针对这样的情况,C# 为我们提供了并发集合,可以很好的解决上面的问题,

并发集合所在名称空间是 System.Collections.Concurrent,其下的集合类如下表:

System.Collections.Concurrent  
BlockingCollection<T> 为实现 IProducerConsumerCollection<T> 的线程安全集合提供阻塞和限制功能。
ConcurrentBag<T> 表示对象的线程安全的无序集合。
ConcurrentDictionary<TKey,TValue> 表示可由多个线程同时访问的键/值对的线程安全集合。
ConcurrentQueue<T> 表示线程安全的先进先出 (FIFO) 集合。
ConcurrentStack<T> 表示线程安全的后进先出 (LIFO) 集合。
   

下面我们改用线程安全的 ConcurrentBag<T> 来实现,如下。

将 Exam.cshtml.cs 中代码修改如下:

namespace ConcurrencyDemo.Pages
{
    public class ExamModel : PageModel
    {

        public void OnGet()
        {
            CreateQuestions22();
        }
     //使用线程安全的并发集合 ConcurrentBag<T>
        public ConcurrentBag<string> QuestionList = new ConcurrentBag<string>();
        private void CreateQuestions22()
        {
            Parallel.For(1, 101, i => //多线程循环100次,期望在泛型集合QuestionList中写入100个题目
            {
                Thread.Sleep(90); //模拟一个耗时操作
                QuestionList.Add("题目 " + i.ToString()); //将数据写入到集合中,不同的题目用 i 来区分
            });
        }
   } }

Exam.cshtml 中代码修改如下:

@page
@model ConcurrencyDemo.Pages.ExamModel
@{
    ViewData["Title"] = "Exam";
}

<table border="1">
    <tr>
        @{int i = 0; } //定义变量 i 作为页面列表编号
        @while (!Model.QuestionList.IsEmpty) //遍历集合 ConcurrentBag<string>
        {
            @if (Model.QuestionList.TryTake(out string ques)) //集合元素取值
            {
                <td style="height:30px;border:solid 1px #c0c0c0;">
                    @(i+1)
                    <input type="radio" name="yesorno" value="1"><span style="color:blue;"></span> &nbsp;
                    <input type="radio" name="yesorno" value="0"><span style="color:blue;"></span>
                    &nbsp;&nbsp;&nbsp;&nbsp; @ques
                </td>
                if (i % 10 == 9)
                {
                    @Html.Raw("</tr><tr>");
                }
                i = i + 1;
            }
        }
    </tr>
</table>

编译后运行页面得到如下结果(注意这里遍历并发集合 ConcurrentBag<string> 的写法,和 List<string> 是不同的):

可以看到,写入了 100 个题目,符合我们的期望。

其余的并发集合用法类似,具体项目中根据情况来选择,这里不一一介绍了。

 

posted @ 2021-06-13 10:07  屏风马  阅读(569)  评论(0编辑  收藏  举报