并发编程 - 线程同步(八)之自旋锁SpinLock

打印 上一主题 下一主题

主题 927|帖子 927|积分 2796

前面对互斥锁Monitor进行了详细学习,今天我们将继续学习,一种更轻量级的锁——自旋锁SpinLock。

在 C# 中,SpinLock是一个高效的自旋锁实现,用于提供一种轻量级的锁机制。SpinLock通过在期待锁的过程中执行自旋(即不断尝试获取锁)来避免线程上下文切换,从而减少系统开销。

SpinLock是一个结构体,使用上和Monitor类很像,都是通过Enter或TryEnter方法持有锁,同时默认支持lockTaken模式,然后通过Exit释放锁。
01、使用示例

下面我们通过启动10个线程,使用SpinLock锁分别递增共享变量_counter,末了再打印出共享变量_counter,代码如下:
  1. public class SpinLockExample
  2. {
  3.     //自旋锁
  4.     private static SpinLock _spinLock = new SpinLock();
  5.     //共享资源计数器
  6.     private static int _counter = 0;
  7.     //计数
  8.     public void Count()
  9.     {
  10.         var lockTaken = false;
  11.         try
  12.         {
  13.             //持有锁
  14.             _spinLock.Enter(ref lockTaken);
  15.             //访问并修改共享资源
  16.             _counter++;
  17.             var threadId = Thread.CurrentThread.ManagedThreadId;
  18.             Console.WriteLine($"线程号:{threadId} 递增共享变量 _counter 为:{_counter}");
  19.         }
  20.         finally
  21.         {
  22.             if (lockTaken)
  23.             {
  24.                 //释放锁
  25.                 _spinLock.Exit();
  26.             }
  27.         }
  28.     }
  29.     //打印
  30.     public void Print()
  31.     {
  32.         Console.WriteLine($"---------------------------------------");
  33.         Console.WriteLine($"_counter 最终值为:{_counter}");
  34.     }
  35. }
  36. public static void SpinLockRun()
  37. {
  38.     var example = new SpinLockExample();
  39.     //启动10个线程
  40.     var threads = new Thread[10];
  41.     for (var i = 0; i < threads.Length; i++)
  42.     {
  43.         threads[i] = new Thread(example.Count);
  44.         threads[i].Start();
  45.     }
  46.     for (var i = 0; i < threads.Length; i++)
  47.     {
  48.         threads[i].Join();
  49.     }
  50.     example.Print();
  51. }
复制代码
不消看也可以猜测出结果为10,执行结果如下:

另外TryEnter方法也和Monitor类同样支持设置超时时间。
02、小心传递SpinLock实例

在传递SpinLock实例时,需要十分小心,这是因为SpinLock是结构体即为值类型,当通过值传递,会导致创建该结构体的副本,复制一个新的实例,而不是传递引用。
如下示例代码:
  1. public class CopySpinLockExample
  2. {
  3.     public void Method1(int thread, SpinLock lockCopy)
  4.     {
  5.         var lockTaken = false;
  6.         //尝试获取锁
  7.         lockCopy.Enter(ref lockTaken);
  8.         if (lockTaken)
  9.         {
  10.             Console.WriteLine($"线程 {thread},成功获取锁");
  11.         }
  12.         else
  13.         {
  14.             Console.WriteLine("线程 {thread},未获取到锁");
  15.         }
  16.     }
  17. }
  18. public static void CopySpinLockRun()
  19. {
  20.     var example = new CopySpinLockExample();
  21.     SpinLock spinLock = new SpinLock();
  22.     example.Method1(1, spinLock);
  23.     example.Method1(2, spinLock);
  24.     spinLock.Exit();
  25.     Console.WriteLine("主线程,释放锁");
  26. }
复制代码
这段代码有两个问题是:
1.方法Method1的两次调用中的lockCopy是各不相同的锁,即会导致两次调用都能获取到锁;
2.方法Method1中的lockCop和主方法中spinLock是两个不同的锁,会导致主方法释放锁异常;
我们可以看看代码执行结果:

方法两次调用都成功获取锁,同时末了释放锁时抛出了异常。和我们上面说的两个问题完全一致。
而要办理这个问题也很简单,只需要把Method1方法的SpinLock参数前加上ref即可。代码如下:
  1. public class RefCopySpinLockExample
  2. {
  3.     public void Method1(int thread, ref SpinLock lockCopy)
  4.     {
  5.         var lockTaken = false;
  6.         //尝试获取锁
  7.         lockCopy.Enter(ref lockTaken);
  8.         if (lockTaken)
  9.         {
  10.             Console.WriteLine($"线程 {thread},成功获取锁");
  11.             lockCopy.Exit();
  12.             Console.WriteLine($"线程 {thread},释放锁");
  13.         }
  14.         else
  15.         {
  16.             Console.WriteLine("线程 {thread},未获取到锁");
  17.         }
  18.     }
  19.     public void Method2(int thread, ref SpinLock lockCopy)
  20.     {
  21.         var lockTaken = false;
  22.         //尝试获取锁
  23.         lockCopy.Enter(ref lockTaken);
  24.         if (lockTaken)
  25.         {
  26.             Console.WriteLine($"线程 {thread},成功获取锁");
  27.         }
  28.         else
  29.         {
  30.             Console.WriteLine("线程 {thread},未获取到锁");
  31.         }
  32.     }
  33. }
  34. public static void RefCopySpinLockRun()
  35. {
  36.     var example = new RefCopySpinLockExample();
  37.     SpinLock spinLock = new SpinLock();
  38.     example.Method1(1, ref spinLock);
  39.     example.Method2(2, ref spinLock);
  40.     spinLock.Exit();
  41.     Console.WriteLine("主线程,释放锁");
  42. }
复制代码
执行结果如下:

从结果上可以发现Method中和主方法中的SpinLock锁都是同一个了。
03、实现原理

从上面代码可以发现从使用上来说,SpinLock和互斥锁Monitor根本一样,那为什么还要SpinLock呢?
首先互斥锁Monitor在获取锁时会壅闭线程,同时线程会进行上下文切换,把CPU资源让出来给其他线程使用,直到锁可用。从这里也可以看出互斥锁Monitor实用锁竞争时间较长的场景,否则线程上下文切换比期待资源消耗代价更高就不划算了。
针对上面提到的问题,就引发了需要一种非壅闭线程的锁方案,因此SpinLock就应用而生。
怎样实现非壅闭线程呢?
首先我们需要理解非壅闭的意义,它是为了办理进行线程上下文切换的代价比锁的期待代价更大的问题。说白了就是不要让线程进行上下文切换,好比最简单粗暴的方式就是直接使用while(true){},使得线程一直处于活动状态。
而SpinLock底层实现原理简直通过使用while(true){},使得线程原地停留且又不壅闭线程。因为while(true)自动循环的特点才叫自旋锁。固然SpinLock底层实现不止这么简单,好比还用到了原子操作Interlocked.CompareExchange。
总结下来SpinLock 的工作原理,大致分为以下两步:
1.当前线程尝试获取锁,如果获取成功,进入同步代码块。
2.如果未能取锁(即锁已经被另一个线程持有),则当前线程会在一个循环(自旋)中重复尝试,直到获取到锁。
SpinLock重要优势在于它不会将线程挂起即不会发生线程上下文切换,而是让线程在一个循环(自旋)中期待,直到锁被释放后再获取。同样因为线程一直自旋期待,如果线程需要期待时间很长又会导致CPU占用过高以及资源浪费。
结合SpinLock实现原理,有如下建议:
1.在需要大量锁(高并发)并且锁持有时间又非常短的场景下,特别恰当使用SpinLock。
2.避免在单核CPU上使用SpinLock,因为自旋期待会浪费CPU资源。
04、实现一个简单的自旋锁

下面我们可以根据SpinLock实现原理来本身实现一个简单的自旋锁。
大致思路如下:
1.通过在while(true)循环中,使用原子操作Interlocked.CompareExchange进行设置锁,从而实现持有锁方法Enter;
2.通过直接标记锁状态为未锁定状态,来实现锁释放方法Exit;
具体代码如下:
  1. public class MySpinLock
  2. {
  3.     // 0 - 未锁定, 1 - 锁定
  4.     private volatile int _isLocked = 0;  
  5.     //获取锁
  6.     public void Enter()
  7.     {
  8.         while (true)
  9.         {
  10.             //使用原子操作检查和设置锁
  11.             if (Interlocked.CompareExchange(ref _isLocked, 1, 0) == 0)
  12.             {
  13.                 //成功获得锁
  14.                 return;
  15.             }
  16.         }
  17.     }
  18.     //释放锁
  19.     public void Exit()
  20.     {
  21.         //释放锁,直接设置为未锁定状态
  22.         _isLocked = 0;
  23.     }
  24. }
复制代码
然后把使用示例中的代码SpinLock替换为MySpinLock即可验证我们本身的自旋锁实现,运行结果如下,根本和原生的SpinLock功能一致。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大连全瓷种植牙齿制作中心

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表