接昨天谈及的线程同步问题,今天介绍一个比较简单的类,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 语句会等待资源的释放,直至访问成功完成任务;而本篇中我们的线程会视图访问一些资源,不成功时我们会干别的事情,不会等待。
希望在这一系列文章写完的时候,我会对线程的同步有一个正确且深刻的认识,这也是我写这些读书笔记的目的。