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> <input type="radio" name="yesorno" value="0"><span style="color:blue;">错</span> @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) <input type="radio" name="yesorno" value="1"><span style="color:blue;">对</span> <input type="radio" name="yesorno" value="0"><span style="color:blue;">错</span> @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> <input type="radio" name="yesorno" value="0"><span style="color:blue;">错</span> @ques </td> if (i % 10 == 9) { @Html.Raw("</tr><tr>"); } i = i + 1; } } </tr> </table>
编译后运行页面得到如下结果(注意这里遍历并发集合 ConcurrentBag<string> 的写法,和 List<string> 是不同的):
可以看到,写入了 100 个题目,符合我们的期望。
其余的并发集合用法类似,具体项目中根据情况来选择,这里不一一介绍了。