将字典重构为并发字典

本文关键字:字典 并发 重构 | 更新日期: 2023-09-27 17:56:53

我想让我的代码多线程,因此我需要将字典更改为ConcurrentDictionary。我阅读了有关ConcurrentDictionary的信息,检查了一些示例,但我仍然需要帮助:

这是原始代码(用于单线程)

private IDictionary<string, IDictionary<string, Task>> _tasks;
public override IDictionary<string, IDictionary<string, Task>> Tasks
{
    get
    {
        // return dictionary from cache unless too old
        // concurrency!! (null check)
        if (_tasks != null && (DateTime.Now - _lastTaskListRefreshDateTime < TimeSpan.FromSeconds(30)))
        {
            return _tasks;
        }
        // reload dictionary from database
        _tasks = new Dictionary<string, IDictionary<string, Task>>();
        // find returns an IEnumerable<Task>
        var tasks = Find<Task>(null, DependencyNode.TaskForCrawler).Cast<Task>();
        // build hierarchical dictionary from flat IEnumerable
        // concurrency!!
        foreach (var t in tasks)
        {
            if (_tasks.ContainsKey(t.Area.Key))
            {
                if (_tasks[t.Area.Key] == null)
                {
                    _tasks[t.Area.Key] = new Dictionary<string, Task>();
                }
                if (!_tasks[t.Area.Key].ContainsKey(t.Key))
                {
                    _tasks[t.Area.Key].Add(t.Key, t);
                }
            }
            else
            {
                _tasks.Add(t.Area.Key, new Dictionary<string, Task> { { t.Key, t } });
            }
        }
        _lastTaskListRefreshDateTime = DateTime.Now;
        return _tasks;
    }
    set
    {
        _tasks = value;
    }
}

这是我想到的:

private ConcurrentDictionary<string, ConcurrentDictionary<string, Task>> _tasks = new ConcurrentDictionary<string, ConcurrentDictionary<string, Task>>();
public override ConcurrentDictionary<string, ConcurrentDictionary<string, Task>> Tasks
{
    get
    {
        // use cache
        // concurrency?? (null check)
        if (!_tasks.IsEmpty && (DateTime.Now - _lastTaskListRefreshDateTime < TimeSpan.FromSeconds(30)))
        {
            return _tasks;
        }
        // reload
        var tasks = Find<Task>(null, DependencyNode.TaskForCrawler).Cast<Task>();
        foreach (var task in tasks)
        {
            var t = task; // inner scope for clousure
            var taskKey = t.Key;
            var areaKey = t.Area.Key;
            var newDict = new ConcurrentDictionary<string, Task>();
            newDict.TryAdd(taskKey, t);
            _tasks.AddOrUpdate(areaKey, newDict, (k, v) => {
                                                    // An dictionary element if key=areaKey already exists
                                                    // extend and return it.
                                                    v.TryAdd(taskKey, t);
                                                    return v;
                                                   });
        }
        _lastTaskListRefreshDateTime = DateTime.Now;
        return _tasks;
    }
}

我不太确定是这样,特别是我很确定IsEmpty检查不是线程安全的,因为_tasks可能已在IsEmpty检查和&& ...部分或return _tasks部分之间初始化。我必须手动锁定此检查吗?我需要双重锁(空检查>锁>空检查)吗?

将字典重构为并发字典

你的担心是有道理的。Tasks属性 getter 不是线程安全的。这里有几个问题。

首先,就像你这边一样,从一个线程调用IsEmpty和从另一个线程中删除项目之间存在竞争。getter 可以返回一个空字典。

其次,在if检查中读取_lastTaskListRefreshDateTime和在获取器结束时进行分配之间存在竞争。即使这些操作是原子的(至少在 32 位平台上不能这样做,因为DateTime是 64 位),仍然存在一个微妙的内存屏障问题,因为代码中没有明显的volatile同步机制。

第三,与我上面的解释类似,_tasks参考还有另一个记忆障碍问题。一个线程可以调用 setter,而另一个线程正在调用 getter。由于不存在内存屏障,因此 CLR 或硬件可以自由地优化读取和写入,以使 getter 看不到在 setter 中所做的更改。这个问题不一定会导致任何问题,但我敢打赌这是没有预料到的行为。由于没有其他分析背景,我不能说任何一种方式。

ConcurrentDictionary只保证读取和写入字典不会相互遍历,这是Dictionary类不会做的。ConcurrentDictionary中的线程安全不会使代码线程安全,它只会确保其代码是线程安全的。在这种情况下,您将需要锁定您的吸气器。