【问题标题】:Chance of hitting the same function at the same time by two Threads/Tasks两个线程/任务同时达到相同功能的机会
【发布时间】:2017-04-01 10:14:48
【问题描述】:

假设如下情况:

public HashTable map = new HashTable();
public void Cache(String fileName) {
        if (!map.ContainsKey(fileName))
        {
            map.Add(fileName, new Object());
            _Cache(fileName);
        }
    }
}
private void _Cache(String fileName) {
        lock (map[fileName])
        {
            if (File Already Cached)
                return;
            else {
                cache file
            } 
        }
    } 

当有以下消费者时:

Task.Run(()=> {
    Cache("A");
});
Task.Run(()=> {
    Cache("A");
});

Cache 方法是否有可能以任何方式抛出 Duplicate key 异常,这意味着两个任务都会命中 map.add 方法并尝试添加相同的键??

编辑:

使用下面的数据结构能解决这个并发问题吗?

public class HashMap<Key, Value>
{
    private HashSet<Key> Keys = new HashSet<Key>();
    private List<Value> Values = new List<Value>();
    public int Count => Keys.Count;
    public Boolean Add(Key key, Value value) {
        int oldCount = Keys.Count;
        Keys.Add(key);
        if (oldCount != Keys.Count) {
            Values.Add(value);
            return true;
        }
        return false;
    }
}

【问题讨论】:

    标签: c# multithreading locking task


    【解决方案1】:

    是的,当然有可能。考虑以下片段:

        if (!map.ContainsKey(fileName))
        {
            map.Add(fileName, new Object());
    

    线程1可能会执行if (!map.ContainsKey(fileName)),发现map中不包含key,所以会继续添加它,但在它有机会添加之前,线程2也可能执行if (!map.ContainsKey(fileName)),此时点它也会发现map不包含key,所以也会继续添加。当然,那会失败。

    编辑(澄清后)

    所以,问题似乎是如何尽可能少地保持主map锁定,以及如何防止缓存对象被初始化两次。

    这是一个复杂的问题,所以我不能给你一个可以运行的现成答案,(特别是因为我目前什至没有方便的 C# 开发环境,)但一般来说,我认为你应该进行如下操作:

    1. lock() 全面保护您的map

    2. 尽可能少地锁定map;当在地图中找不到对象时,将empty对象添加到地图中并立即退出锁定。这将确保此地图不会成为所有进入 Web 服务器的请求的争用点。

    3. 在 check-if-present-and-add-if-not 片段之后,您将持有一个保证在地图中的对象。但是,此时此对象可能会也可能不会被初始化。没关系。接下来我们会处理这个问题。

    4. 重复 lock-and-check 习惯用法,这次是缓存对象:每个对特定对象感兴趣的传入请求都需要锁定它,检查它是否已初始化,如果没有,则初始化它。当然,只有第一个请求会遭受初始化的惩罚。此外,在对象完全初始化之前到达的任何请求都必须等待它们的lock,直到对象被初始化。但这一切都很好,这正是你想要的。

    【讨论】:

    • 没错,另一个问题,当有一系列并发任务时(例如缓存文件)。 Task_A 尝试请求File_A,Task_B 尝试同时请求File_A。现在我想检查文件是否已缓存,如果没有缓存,我希望 Task_A 进入锁,我希望 Task_B 等待缓存完成。你有什么推荐的?
    • 您的缓存需要使用synchronized 关键字进行全面保护。
    • 这是 C# 不是 Java,我认为这只适用于 Java。
    • 不,您的编辑不会解决您的并发问题。考虑一下如果Add() 的一次调用被Add() 的另一次调用抢占在int oldCount = Keys.Count; 之后和Keys.Add(key); 之前会发生什么此外,HashSet 不是线程安全的,所以即使是单独的Keys.Add(key); 也会崩溃和在比赛条件下燃烧。一般来说,没有任何聪明的技巧可以解决任何并发问题,而无需使用lock() 或其他适当形式的同步。
    • 这是一个绝妙的主意。你猜怎么着?它广泛使用lock()。你应该接受这是生活中的事实,并克服你认为lock() 导致堆积的不合理观念。错误使用lock() 会导致堆积。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-18
    • 1970-01-01
    相关资源
    最近更新 更多