代码片段涉及一个 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()));
    }
}

问题分析:

  1. 集合被修改:

    • SortedDictionary 在遍历期间(例如通过 foreach 或多次访问)若被其他线程修改,会抛出 InvalidOperationException。

    • 即使你的代码没有直接使用 foreach,多次调用 ContainsKey 和索引访问(sortedNtc[i])仍可能受到并发修改的影响。

  2. 性能问题:

    • 每次循环调用 ContainsKey 两次(检查 i 和 i + 1),再通过索引访问值,效率较低。

    • Convert.ToInt32(NTCStation) 在循环中重复执行,增加开销。

    • string.Format 和字符串拼接(假设 str 是 string 类型)性能较差。

  3. 异常风险:

    • Convert.ToInt32(NTCStation) 未处理无效输入,可能抛出 FormatException 或 OverflowException。

    • 未检查 sortedNtc 是否为 null 或空。

  4. 可读性:

    • 条件嵌套较深,逻辑复杂。

    • 字符串格式化代码重复,维护性较差。

目标:

  • 防止“集合被修改”异常。

  • 优化性能和可读性。

  • 提供不加锁和加锁两种方案。


优化方案 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)}");
    }
}

优化点

  1. 解决“集合被修改”:

    • 使用 ConcurrentDictionary 替代 SortedDictionary,其内部通过细粒度锁实现线程安全,即使其他线程修改集合也不会抛出异常。

    • 或者,可以创建 sortedNtc 的副本(如 new Dictionary<int, double>(sortedNtc)),但 ConcurrentDictionary 更适合并发场景。

  2. 性能提升:

    • 使用 TryGetValue 替代 ContainsKey 和索引访问,减少字典查找次数(从最多三次降为最多两次)。

    • 提前解析 ntcStation 并缓存 stationLimit,避免循环中重复转换。

    • 使用 StringBuilder(假设 str 是 StringBuilder)优化字符串拼接。

    • 使用字符串插值($)替代 string.Format,代码更简洁。

  3. 异常处理:

    • 使用 int.TryParse 安全解析 ntcStation,避免 FormatException 或 OverflowException。

    • 如果 sortedNtc 为 null,ConcurrentDictionary 构造函数会处理空输入。

  4. 排序问题:

    • ConcurrentDictionary 不保证键的排序(与 SortedDictionary 不同)。如果需要排序,可以在操作后对结果排序(见下文扩展)。

  5. 可读性:

    • 逻辑清晰,条件判断简化。

    • 变量名(如 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)}");
        }
    }
}

优化点

  1. 解决“集合被修改”:

    • lock (sortedNtc) 确保同一时间只有一个线程访问 sortedNtc,防止并发修改导致异常。

    • 遍历期间集合保持一致性。

  2. 性能提升:

    • 使用 TryGetValue 减少字典查找次数。

    • 提前解析 ntcStation 并缓存 stationLimit。

    • 使用 StringBuilder 和字符串插值优化字符串操作。

  3. 异常处理:

    • 使用 int.TryParse 避免解析异常。

  4. 排序保留:

    • 保留 SortedDictionary 的键排序特性,无需额外排序。

  5. 可读性:

    • 代码结构清晰,逻辑简洁。

替代锁:使用 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)

是(多读单写)

高,适合读多写少

读频繁,需排序


进一步优化建议

  1. 输入验证:

    • 检查 sortedNtc 是否为 null 或空:

      csharp

      if (sortedNtc == null || sortedNtc.Count == 0) return;
  2. 预过滤键:

    • 如果只需要特定范围的键,可以提前过滤:

      csharp

      var relevantKeys = sortedNtc.Keys.Where(k => k >= 1 && k < 16).ToList();
  3. 格式化优化:

    • 如果输出字符串需要去掉前导逗号,可以在循环后处理:

      csharp

      if (str.Length > 0) str.Remove(0, 1);
  4. 死锁预防(加锁方案):

    • 确保锁对象(如 sortedNtc)在所有代码段中一致使用。

    • 避免在锁内执行长时间操作。


总结

  • 不加锁方案:

    • 使用 ConcurrentDictionary 或集合副本,避免“集合被修改”异常。

    • 适合高并发或修改频繁的场景,但可能需手动排序。

  • 加锁方案:

    • 使用 lock(简单)或 ReaderWriterLockSlim(高效)保护 SortedDictionary。

    • 适合读多写少或需要排序的场景。

  • 推荐:

    • 高并发、排序不重要:使用 ConcurrentDictionary(不加锁)。

    • 需排序、低并发:使用 lock(加锁)。

    • 读频繁、需排序:使用 ReaderWriterLockSlim(加锁)。

  • 通用优化:

    • 使用 TryGetValue 和 StringBuilder 提高性能。

    • 安全解析 NTCStation 避免异常。

如果有特定场景(如集合大小、并发频率、排序需求),请提供更多细节以进一步优化。

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐