对“集合被修改”问题的解释,并提供不加锁和加锁两种优化方案,解决异常风险并提升代码性能和可读性
代码片段涉及一个 SortedDictionary<int, double> 集合 sortedNtc,用于存储整数键和双精度浮点值。代码遍历该集合并根据条件读取值,追加到字符串中。问题在于,如果在遍历过程中,另一个线程修改了 sortedNtc(例如添加或删除键值对),会导致“集合被修改”异常(InvalidOperationException: Collection was modified),因为 SortedDictionary 不是线程安全的。
以下是对“集合被修改”问题的解释,并提供不加锁和加锁两种优化方案,解决异常风险并提升代码性能和可读性。
原始代码问题
原始代码如下:
csharp
SortedDictionary<int, double> sortedNtc = new SortedDictionary<int, double>(ntc);
string NTCStation = ConfigurationManager.AppSettings["NTCStation"];
for (int i = 1; i < 16; i += 2)
{
if (!string.IsNullOrEmpty(NTCStation))
{
if (i >= Convert.ToInt32(NTCStation) * 2)
continue;
}
if (sortedNtc.ContainsKey(i))
{
str.Append(string.Format(",{0}", Math.Round(sortedNtc[i], 2).ToString()));
}
else if (sortedNtc.ContainsKey(i + 1))
{
str.Append(string.Format(",{0}", Math.Round(sortedNtc[i + 1], 2).ToString()));
}
}
问题分析:
-
集合被修改:
-
SortedDictionary 在遍历期间(例如通过 foreach 或多次访问)若被其他线程修改,会抛出 InvalidOperationException。
-
即使你的代码没有直接使用 foreach,多次调用 ContainsKey 和索引访问(sortedNtc[i])仍可能受到并发修改的影响。
-
-
性能问题:
-
每次循环调用 ContainsKey 两次(检查 i 和 i + 1),再通过索引访问值,效率较低。
-
Convert.ToInt32(NTCStation) 在循环中重复执行,增加开销。
-
string.Format 和字符串拼接(假设 str 是 string 类型)性能较差。
-
-
异常风险:
-
Convert.ToInt32(NTCStation) 未处理无效输入,可能抛出 FormatException 或 OverflowException。
-
未检查 sortedNtc 是否为 null 或空。
-
-
可读性:
-
条件嵌套较深,逻辑复杂。
-
字符串格式化代码重复,维护性较差。
-
目标:
-
防止“集合被修改”异常。
-
优化性能和可读性。
-
提供不加锁和加锁两种方案。
优化方案 1:不加锁
不加锁方案通过创建集合副本或使用线程安全集合(如 ConcurrentDictionary)来避免修改原始集合时的异常。这种方法适合不需要同步多线程访问的场景,或者希望最小化锁的开销。
优化代码(不加锁)
csharp
using System.Collections.Concurrent;
using System.Text;
// 将 SortedDictionary 转换为 ConcurrentDictionary 或创建副本
ConcurrentDictionary<int, double> sortedNtc = new ConcurrentDictionary<int, double>(ntc);
StringBuilder str = new StringBuilder();
string ntcStation = ConfigurationManager.AppSettings["NTCStation"];
int stationLimit = 0;
// 安全解析 NTCStation
if (!string.IsNullOrEmpty(ntcStation) && int.TryParse(ntcStation, out int station))
{
stationLimit = station * 2;
}
// 遍历并读取值
for (int i = 1; i < 16; i += 2)
{
if (stationLimit > 0 && i >= stationLimit)
continue;
if (sortedNtc.TryGetValue(i, out double value) || sortedNtc.TryGetValue(i + 1, out value))
{
str.Append($",{Math.Round(value, 2)}");
}
}
优化点
-
解决“集合被修改”:
-
使用 ConcurrentDictionary 替代 SortedDictionary,其内部通过细粒度锁实现线程安全,即使其他线程修改集合也不会抛出异常。
-
或者,可以创建 sortedNtc 的副本(如 new Dictionary<int, double>(sortedNtc)),但 ConcurrentDictionary 更适合并发场景。
-
-
性能提升:
-
使用 TryGetValue 替代 ContainsKey 和索引访问,减少字典查找次数(从最多三次降为最多两次)。
-
提前解析 ntcStation 并缓存 stationLimit,避免循环中重复转换。
-
使用 StringBuilder(假设 str 是 StringBuilder)优化字符串拼接。
-
使用字符串插值($)替代 string.Format,代码更简洁。
-
-
异常处理:
-
使用 int.TryParse 安全解析 ntcStation,避免 FormatException 或 OverflowException。
-
如果 sortedNtc 为 null,ConcurrentDictionary 构造函数会处理空输入。
-
-
排序问题:
-
ConcurrentDictionary 不保证键的排序(与 SortedDictionary 不同)。如果需要排序,可以在操作后对结果排序(见下文扩展)。
-
-
可读性:
-
逻辑清晰,条件判断简化。
-
变量名(如 stationLimit)更具描述性。
-
扩展:如果需要排序
若必须保留 SortedDictionary 的键排序特性,可以在操作前创建有序副本:
csharp
// 创建有序副本
var sortedSnapshot = new SortedDictionary<int, double>(sortedNtc);
for (int i = 1; i < 16; i += 2)
{
if (stationLimit > 0 && i >= stationLimit)
continue;
if (sortedSnapshot.TryGetValue(i, out double value) || sortedSnapshot.TryGetValue(i + 1, out value))
{
str.Append($",{Math.Round(value, 2)}");
}
}
-
优点:副本(sortedSnapshot)是静态的,遍历期间不受原始集合修改影响,保留排序。
-
缺点:需要额外内存存储副本,适合集合较小或修改不频繁的场景。
优化方案 2:加锁
加锁方案通过同步机制(如 lock 或 ReaderWriterLockSlim)保护对 SortedDictionary 的访问,确保遍历期间集合不被修改。适合需要保留 SortedDictionary 排序特性且多线程访问频繁的场景。
优化代码(加锁,使用 lock)
csharp
using System.Text;
using System.Collections.Generic;
SortedDictionary<int, double> sortedNtc = new SortedDictionary<int, double>(ntc);
StringBuilder str = new StringBuilder();
string ntcStation = ConfigurationManager.AppSettings["NTCStation"];
int stationLimit = 0;
// 安全解析 NTCStation
if (!string.IsNullOrEmpty(ntcStation) && int.TryParse(ntcStation, out int station))
{
stationLimit = station * 2;
}
// 使用 lock 保护遍历
lock (sortedNtc)
{
for (int i = 1; i < 16; i += 2)
{
if (stationLimit > 0 && i >= stationLimit)
continue;
if (sortedNtc.TryGetValue(i, out double value) || sortedNtc.TryGetValue(i + 1, out value))
{
str.Append($",{Math.Round(value, 2)}");
}
}
}
优化点
-
解决“集合被修改”:
-
lock (sortedNtc) 确保同一时间只有一个线程访问 sortedNtc,防止并发修改导致异常。
-
遍历期间集合保持一致性。
-
-
性能提升:
-
使用 TryGetValue 减少字典查找次数。
-
提前解析 ntcStation 并缓存 stationLimit。
-
使用 StringBuilder 和字符串插值优化字符串操作。
-
-
异常处理:
-
使用 int.TryParse 避免解析异常。
-
-
排序保留:
-
保留 SortedDictionary 的键排序特性,无需额外排序。
-
-
可读性:
-
代码结构清晰,逻辑简洁。
-
替代锁:使用 ReaderWriterLockSlim
如果读操作远多于写操作,ReaderWriterLockSlim 更高效,因为它允许多线程同时读取:
csharp
using System.Text;
using System.Threading;
using System.Collections.Generic;
SortedDictionary<int, double> sortedNtc = new SortedDictionary<int, double>(ntc);
StringBuilder str = new StringBuilder();
string ntcStation = ConfigurationManager.AppSettings["NTCStation"];
int stationLimit = 0;
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
if (!string.IsNullOrEmpty(ntcStation) && int.TryParse(ntcStation, out int station))
{
stationLimit = station * 2;
}
// 使用读锁
rwLock.EnterReadLock();
try
{
for (int i = 1; i < 16; i += 2)
{
if (stationLimit > 0 && i >= stationLimit)
continue;
if (sortedNtc.TryGetValue(i, out double value) || sortedNtc.TryGetValue(i + 1, out value))
{
str.Append($",{Math.Round(value, 2)}");
}
}
}
finally
{
rwLock.ExitReadLock();
}
-
优点:允许多线程同时读取,提高并发性能。
-
缺点:代码稍复杂,需确保锁正确释放。
两种方案对比
|
方案 |
线程安全 |
排序支持 |
性能 |
适用场景 |
|---|---|---|---|---|
|
不加锁(ConcurrentDictionary) |
是(内部细粒度锁) |
否(需手动排序) |
高,适合高并发 |
高并发,排序要求不高 |
|
不加锁(副本) |
是(静态副本) |
是 |
中,额外内存开销 |
单次遍历,集合较小 |
|
加锁(lock) |
是(独占锁) |
是 |
中,独占锁降低并发 |
低并发,需排序 |
|
加锁(ReaderWriterLockSlim) |
是(多读单写) |
是 |
高,适合读多写少 |
读频繁,需排序 |
进一步优化建议
-
输入验证:
-
检查 sortedNtc 是否为 null 或空:
csharp
if (sortedNtc == null || sortedNtc.Count == 0) return;
-
-
预过滤键:
-
如果只需要特定范围的键,可以提前过滤:
csharp
var relevantKeys = sortedNtc.Keys.Where(k => k >= 1 && k < 16).ToList();
-
-
格式化优化:
-
如果输出字符串需要去掉前导逗号,可以在循环后处理:
csharp
if (str.Length > 0) str.Remove(0, 1);
-
-
死锁预防(加锁方案):
-
确保锁对象(如 sortedNtc)在所有代码段中一致使用。
-
避免在锁内执行长时间操作。
-
总结
-
不加锁方案:
-
使用 ConcurrentDictionary 或集合副本,避免“集合被修改”异常。
-
适合高并发或修改频繁的场景,但可能需手动排序。
-
-
加锁方案:
-
使用 lock(简单)或 ReaderWriterLockSlim(高效)保护 SortedDictionary。
-
适合读多写少或需要排序的场景。
-
-
推荐:
-
高并发、排序不重要:使用 ConcurrentDictionary(不加锁)。
-
需排序、低并发:使用 lock(加锁)。
-
读频繁、需排序:使用 ReaderWriterLockSlim(加锁)。
-
-
通用优化:
-
使用 TryGetValue 和 StringBuilder 提高性能。
-
安全解析 NTCStation 避免异常。
-
如果有特定场景(如集合大小、并发频率、排序需求),请提供更多细节以进一步优化。
更多推荐




所有评论(0)