.net 将字典重构为并发字典

xriantvc  于 2023-01-18  发布在  .NET
关注(0)|答案(2)|浏览(143)

我想让我的代码多线程化,因此我需要把字典变成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部分之间初始化了。我必须手动锁定这个检查吗?我需要双重锁定吗(null check〉lock〉null check)?

pkwftd7m

pkwftd7m1#

您的担心是有道理的。Tasks属性getter不是线程安全的。这里有几个问题。
首先,就像你所说的,在一个线程调用IsEmpty和从另一个线程移除item之间存在竞争,getter可能返回一个空字典。
第二,在if检查中读取_lastTaskListRefreshDateTime和getter末尾的赋值之间存在竞争,即使这些操作是原子的(至少在32位平台上不能是原子的,因为DateTime是64位的),仍然存在一个微妙的内存障碍问题,因为代码中没有像volatile这样的同步机制。
第三,与我上面的解释类似,_tasks引用还有另一个内存障碍问题。一个线程可能调用setter,而另一个线程调用getter。由于不存在内存障碍,CLR或硬件可以自由地优化读写,以使setter中所做的更改对getter不可见。这个问题可能不一定会导致任何问题。但我敢打赌,这是没有预料到的行为。由于没有其他背景进行分析,我不能说任何一种方式。

bvjveswy

bvjveswy2#

ConcurrentDictionary只能保证字典的读写不会互相遍历,Dictionary类不能做到这一点。ConcurrentDictionary中的线程安全并不能保证你的代码是线程安全的,它只能保证它的代码是线程安全的。既然是这样,你需要在getter中设置一个锁。

相关问题