C#线程同步技术(二) Interlocked 类

接昨天谈及的线程同步问题,今天介绍一个比较简单的类,Interlocked。它提供了以线程安全的方式递增、递减、交换和读取值的方法。

它的特点是:

1、相对于其他线程同步技术,速度会快很多。

2、只能用于简单的同步问题。

比叫好理解,不再赘述,给一个我们常用的单例模式的 Interlocked 实现:

        class SourceManager
        {
            private SourceManager() { }

            private static SourceManager sourceManager;
            public static SourceManager Instance
            {
                get
                {
                    if (sourceManager == null)
                    {
                        /*
                         
                        lock 实现方式
                        功能与以下 Interlocked.CompareExchange 相同
                         
                        lock (this)
                        {
                        if (sourceManager == null)
                        {
                            sourceManager = new SourceManager();
                        }
                        }
                         
                        */
                        Interlocked.CompareExchange<SourceManager>(ref sourceManager, new SourceManager(), null);
                    }
                    return sourceManager;
                }
            }
        }

Interlocked 类用于使变量的简单语句原子化。再用一个例子说明用 Interlocked 实现线程安全资源锁定机制。

在这个例子中,我们会建立10任务,每个任务会分别循环50000次请求使用资源,而这种资源我们限定同一时间只能有一个线程访问,请求成功则递增 accessed 值,失败则递增 denied 值,因此按我们的预期,accessed 和 denied 的和将会始终是 10*50000 = 500000。且看我们设计的机制:

    class InterlockedCase
    {
        private static int accessed = 0;
        private static int denied = 0;

        // 0 没有线程在使用 1 有线程正在使用
        private static int usingResource = 0;

        private const int nTaskIterations = 50000;
        private const int nTasks = 10;

        public static void Test()
        {
            Task[] tasks = new Task[nTasks];
            for (int i = 0; i < nTasks; i++)
            {
                tasks[i] = Task.Factory.StartNew(ThreadProc);
            }
            for (int i = 0; i < nTasks; i++)
            {
                tasks[i].Wait();
            }
            Console.WriteLine("accessed:{0}, denied:{1}, total:{2}", accessed, denied, accessed+denied);
        }

        private static void ThreadProc()
        {
            for (int i = 0; i < nTaskIterations; i++)
            {
                UseResource();
            }
        }

        private static bool UseResource()
        {
            if (usingResource == 0)
            {
                usingResource = 1;

                accessed++;

                usingResource = 0;
                return true;
            }
            else
            {
                Interlocked.Increment(ref denied);
                return false;
            }
        }
    }

上面例子的运行结果total值却不是我们预期的总请求数 50000!

在代码中,我们设计了一个访问共享资源的逻辑

if (usingResource == 0)
{
  usingResource = 1;

  accessed++;

  usingResource = 0;
  return true;
}

错误的原因是我们控制资源的逻辑里 usingResource 的判断和赋值操作并不是原子操作,会导致有多个线程能同时进入内层操纵资源,修改 accessed!导致 accessed 值的统计不准确!

找到原因,我们把 usingResource 的判断和赋值转为原子操作,就能实现我们的构想了,Interlocked 类正好派上用场!

改造 UseResource() 函数,输出结果正式我们期望的 500000

        private static bool UseResource()
        {
            if (Interlocked.Exchange(ref usingResource,1) == 0)
            {
                accessed++;
                usingResource = 0;
                return true;
            }
            else
            {
                Interlocked.Increment(ref denied);
                return false;
            }
        }

读到这里,有心的朋友可能会问,usingResource 变量为何设计成整型值?用布尔值不好吗?这正是体现整型值的灵活的地方,我们可以通过更改 UseResource() 函数的逻辑,控制统一时间可以有多少个线程访问资源,而并非只限定一个线程可以访问。

后话:

这是第二篇关于线程同步的学习笔记,其实书看得很快,但是文章却写得很慢。我发觉学习线程同步最好的方式就是设计一个反例,并更正它,确认运行结果是否与你预期的一致。在写这篇文章的过程中,我试图设计很多例子,也激发了自己很多的思考,其中有些想法开始是错的,在不断对比思考的过程里,逐渐加深认识了线程资源访问的设计。在大逻辑上,上一篇中 lock 语句会等待资源的释放,直至访问成功完成任务;而本篇中我们的线程会视图访问一些资源,不成功时我们会干别的事情,不会等待。

希望在这一系列文章写完的时候,我会对线程的同步有一个正确且深刻的认识,这也是我写这些读书笔记的目的。

原文地址:https://www.cnblogs.com/cnhxz/p/3861893.html