• ConcurrentDictionary<T,V> 的这两个操作不是原子性的


    好久不见,马甲哥封闭居家半个月,记录之前遇到的一件小事。

    ConcurrentDictionary绝大部分api都是线程安全且原子性的
    唯二的例外是接收工厂委托的api:AddOrUpdateGetOrAdd这两个api不是原子性的,需要引起重视。

    All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd.

    之前有个同事就因为这个case背了一个P。

    AddOrUpdate(TKey, TValue, Func valueFactory);
    GetOrAdd(TKey key, Func valueFactory);
    (注意,包括其他接收工厂委托的重载函数)

    Q1: valueFactory工厂函数不在锁定范围,为什么不在锁范围?

    A: 还不是因为微软不相信你能写出健壮的业务代码,未知的业务代码可能造成死锁。

    However, delegates for these methods are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, the code executed by these delegates is not subject to the atomicity of the operation.

    Q2:带来的效果?

    • valueFactory工厂函数可能会多次执行
    • 虽然会多次执行, 但插入的值永远是一个,插入的值取决于哪个线程率先插入字典。

    Q3: 怎么做到的?
    A: 源代码做了double check了,后续线程通过工厂类创建值后,会再次检查字典,发现已有值,会丢弃自己创建的值。

    示例代码:

    using System.Collections.Concurrent;
    
    public class Program
    {
       private static int _runCount = 0;
       private static readonly ConcurrentDictionary<string, string> _dictionary
           = new ConcurrentDictionary<string, string>();
    
       public static void Main(string[] args)
       {
           var task1 = Task.Run(() => PrintValue("The first value"));
           var task2 = Task.Run(() => PrintValue("The second value"));
           var task3 = Task.Run(() => PrintValue("The three value"));
           var task4 = Task.Run(() => PrintValue("The four value"));
           Task.WaitAll(task1, task2, task4,task4);
           
           PrintValue("The five value");
           Console.WriteLine($"Run count: {_runCount}");
       }
    
       public static void PrintValue(string valueToPrint)
       {
           var valueFound = _dictionary.GetOrAdd("key",
                       x =>
                       {
                           Interlocked.Increment(ref _runCount);
                           Thread.Sleep(100);
                           return valueToPrint;
                       });
           Console.WriteLine(valueFound);
       }
    }
    

    上面4个线程并发插入字典,每次随机输出,_runCount=4显示工厂类执行4次。

    Q4:如果工厂产值的代价很大,不允许多次创建,如何实现?

    笔者的同事之前就遇到这样的问题,高并发请求频繁创建redis连接,直接打挂了机器。

    A: 有一个trick能解决这个问题: valueFactory工厂函数返回Lazy容器.

    using System.Collections.Concurrent;
    
    public class Program
    {
       private static int _runCount2 = 0;
       private static readonly ConcurrentDictionary<string, Lazy<string>> _lazyDictionary
          = new ConcurrentDictionary<string, Lazy<string>>();
    
       public static void Main(string[] args)
       {
           task1 = Task.Run(() => PrintValueLazy("The first value"));
           task2 = Task.Run(() => PrintValueLazy("The second value"));
           task3 = Task.Run(() => PrintValueLazy("The three value"));
           task4 = Task.Run(() => PrintValueLazy("The four value"));    
           Task.WaitAll(task1, task2, task4, task4);
    
           PrintValue("The five value");
           Console.WriteLine($"Run count: {_runCount2}");
       }
    
       public static void PrintValueLazy(string valueToPrint)
       {
           var valueFound = _lazyDictionary.GetOrAdd("key",
                       x => new Lazy<string>(
                           () =>
                           {
                               Interlocked.Increment(ref _runCount2);
                               Thread.Sleep(100);
                               return valueToPrint;
                           }));
           Console.WriteLine(valueFound.Value);
       }
    }
    


    上面示例,依旧会稳定随机输出,但是_runOut=1表明产值动作只执行了一次、

    valueFactory工厂函数返回Lazy容器是一个精妙的trick。

    ① 工厂函数依旧没进入锁定过程,会多次执行;

    ② 与最上面的例子类似,只会插入一个Lazy容器(后续线程依旧做double check发现字典key已经有Lazy容器了,会放弃插入);

    ③ 线程执行Lazy.Value, 这时才会执行创建value的工厂函数;

    ④ 多个线程尝试执行Lazy.Value, 但这个延迟初始化方式被设置为ExecutionAndPublication
    不仅以线程安全的方式执行, 而且确保只会执行一次构造函数。

    public Lazy(Func valueFactory)
      :this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false)
    {
    }
    
    控制构造函数执行的枚举值 描述
    ExecutionAndPublication 能确保只有一个线程能够以线程安全方式执行构造函数
    None 线程不安全
    Publication 并发线程都会执行初始化函数,以先完成初始化的值为准

    IHttpClientFactory在构建<命名HttpClient,活跃连接Handler>字典时, 也用到了这个技巧,大家自行欣赏DefaultHttpCLientFactory源码


    总结

    为解决ConcurrentDictionary GetOrAdd(key, valueFactory) 工厂函数在并发场景下被多次执行的问题。
    ① valueFactory工厂函数产生Lazy容器
    ② 将Lazy容器的值初始化姿势设定为ExecutionAndPublication(线程安全且执行一次)。

    两姿势缺一不可。

  • 相关阅读:
    Java 8 Stream API-流式处理
    cefpython3的使用
    计算机考研自命题(5)
    ChatGPT prompt汇总-个人使用-持续更新....
    tsne 学习使用
    文心一言、讯飞星火与GPT-4/3.5在回答中文历史问题的表现
    企业如何避免项目失败
    Java计算机毕业设计大学生家教管理系统源码+系统+数据库+lw文档
    Power BI DAX 编写利器 —— DaxStudio 的简单用法
    虚拟机信息巡检脚本
  • 原文地址:https://www.cnblogs.com/JulianHuang/p/16698976.html