• .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)


    本随笔续接:.NET 同步与异步之锁(ReaderWriterLockSlim)(八)

    之前的随笔已经说过、加锁虽然能很好的解决竞争条件,但也带来了负面影响:性能方面的负面影响。那有没有更好的解决方案呢?有,原子操作、即 Interlocked 这个类。

    一、让我们先看一个计数的原子操作Demo

            /// <summary>
            /// 原子操作-计数
            /// </summary>
            public void Demo1()
            {
                Task.Run(() =>
                {
                    long total = 0;
                    long result = 0;
    
                    PrintInfo("正在计数");
    
                    Parallel.For(0, 10, (i) =>
                    {
                        for (int j = 0; j < 10000000; j++)
                        {
                            Interlocked.Increment(ref total);
                            result++;
                        }
                    });
    
                    PrintInfo($"操作结果应该为		: {10 * 10000000}");
                    PrintInfo($"原子操作结果		: {total}");
                    PrintInfo($"i++操作结果		: {result}");
                });
            }
    原子操作-计数

    由上述Demo可知、Interlocked 可以很好的保证 64位整型值的计数操作 能否符合预期,而普通的i++操作却出现了竞争条件。

    Interlocked 对于整形操作提供的方法还是很多的,这里不多介绍了。

    二、不一样的单例模式

    Interlocked 中提供了 Interlocked.CompareExchange<T> 方法的泛型版本,让我们来看一下,这个泛型版本的一种巧妙的用法。

            /// <summary>
            /// 原子操作-单例模式
            /// </summary>
            public void Demo2()
            {
                ConcurrentQueue<InterlockedSingleClass> queue = new ConcurrentQueue<Demo.InterlockedSpinLockClass.InterlockedSingleClass>();
    
                // 虽然这个测试不严谨、但也或多或少的说明了一些问题
                for (int i = 0; i < 10; i++) // 同时分配的线程数过多、调度器反而调度不过来
                {
                    Task.Run(() =>
                    {
                        var result = InterlockedSingleClass.SingleInstance;
    
                        queue.Enqueue(result);
                    });
                }
    
    
                // 1秒钟后显示结果
                Task.Delay(1000).ContinueWith((t) =>
                {
                    PrintInfo($"利用原子操作-单例模式、生成的对象总数:{queue.Count}");
    
                    InterlockedSingleClass firstItem = null;
                    queue.TryDequeue(out firstItem);
    
                    for (int i = 0; i < queue.Count;)
                    {
                        InterlockedSingleClass temp = null;
                        queue.TryDequeue(out temp);
    
                        if (temp == null || firstItem == null || !object.ReferenceEquals(temp, firstItem))
                        {
                            PrintInfo("单例模式失效");
                        }
                    }
    
                    PrintInfo("原子操作-单例模式-运行完毕");
                });
    
            }
    
    
            public class InterlockedSingleClass
            {
                private static InterlockedSingleClass single = null;
    
                public static InterlockedSingleClass SingleInstance
                {
                    get
                    {
                        // if (single == null) // 为了测试效果,该行代码注释掉
                        {
                            Interlocked.CompareExchange<InterlockedSingleClass>(ref single, new InterlockedSingleClass(), null);
                        }
    
                        return single;
                    }
                }
    
            }
    原子操作-单例模式

    针对Interlocked.CompareExchange<T>方法、我介绍两句:

    1、第一个参数为 ref 参数,如果第一个参数 和 第三个参数的引用相等,则用第二个参数替换第一个参数的值,并将第一个参数的原始值返回。

    2、该泛型方法 只接受类类型的参数。

    三、自旋锁

    自旋锁:提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。

            /// <summary>
            /// 自旋锁Demo,来源MSDN
            /// </summary>
            public void Demo3()
            {
                SpinLock sl = new SpinLock();
    
                StringBuilder sb = new StringBuilder();
    
                // Action taken by each parallel job.
                // Append to the StringBuilder 10000 times, protecting
                // access to sb with a SpinLock.
                Action action = () =>
                {
                    bool gotLock = false;
                    for (int i = 0; i < 10000; i++)
                    {
                        gotLock = false;
                        try
                        {
                            sl.Enter(ref gotLock);
    
                            sb.Append((i % 10).ToString());
                        }
                        finally
                        {
                            // Only give up the lock if you actually acquired it
                            if (gotLock)
                                sl.Exit();
                        }
                    }
                };
    
                // Invoke 3 concurrent instances of the action above
                Parallel.Invoke(action, action, action);
    
                // Check/Show the results
                PrintInfo($"sb.Length = {sb.Length} (should be 30000)");
    
                PrintInfo($"number of occurrences of '5' in sb: {sb.ToString().Where(c => (c == '5')).Count()} (should be 3000)");
    
            }
    自旋锁

    看完了Demo,让我们再来深入了解一下自旋锁:

    1、自旋锁本身是一个结构、而不是类,这样使用过多的锁时不会造成GC压力。

    2、自旋锁是以一种循环等待的方式去尝试获取锁,也就是说、在等待期间 会一直占用CPU、如果等待时间过长会造成CPU浪费,而 Monitor会休眠(Sleep)。

    3、自旋锁的使用准则:让临界区尽可能短(时间短)、非阻塞的方式。(因为等待时间过长会造成CPU浪费)

    4、由于自旋锁是循环等待的方式、在执行方式上和Monitor的休眠不一样,自旋锁的执行速度会更快。而Monitor的休眠方式会造成额外的系统开销,执行速度反而会降低。

    随笔暂告一段落、下一篇随笔按之前的目录顺序应该是介绍WaitHandler家族的, 笔者临时想变更下顺序、下一遍随笔:并发中的闭包。

    附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

    参见更多:随笔导读:同步与异步


    (未完待续...)

  • 相关阅读:
    [转载]初学C#之list
    List<>过滤重复的简单方法
    C# List<> 删除
    C# 生成随机字符串
    C#正则表达式之字符替换
    c#中怎么删除一个非空目录
    treeview 点击时选中节点
    教程链接
    iOS 允许后台任务吗?
    Git Add,Git别名等
  • 原文地址:https://www.cnblogs.com/08shiyan/p/6479600.html
Copyright © 2020-2023  润新知