并发编程 - 线程同步(八)之自旋锁SpinLock
前面对互斥锁Monitor进行了详细学习,今天我们将继续学习,一种更轻量级的锁——自旋锁SpinLock。

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

SpinLock是一个结构体,使用上和Monitor类很像,都是通过Enter或TryEnter方法持有锁,同时默认支持lockTaken模式,然后通过Exit释放锁。
01、使用示例
下面我们通过启动10个线程,使用SpinLock锁分别递增共享变量_counter,最后再打印出共享变量_counter,代码如下:
public class SpinLockExample
{
//自旋锁
private static SpinLock _spinLock = new SpinLock();
//共享资源计数器
private static int _counter = 0;
//计数
public void Count()
{
var lockTaken = false;
try
{
//持有锁
_spinLock.Enter(ref lockTaken);
//访问并修改共享资源
_counter++;
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程号:{threadId} 递增共享变量 _counter 为:{_counter}");
}
finally
{
if (lockTaken)
{
//释放锁
_spinLock.Exit();
}
}
}
//打印
public void Print()
{
Console.WriteLine($"---------------------------------------");
Console.WriteLine($"_counter 最终值为:{_counter}");
}
}
public static void SpinLockRun()
{
var example = new SpinLockExample();
//启动10个线程
var threads = new Thread[10];
for (var i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(example.Count);
threads[i].Start();
}
for (var i = 0; i < threads.Length; i++)
{
threads[i].Join();
}
example.Print();
}
不用看也可以预测出结果为10,执行结果如下:

另外TryEnter方法也和Monitor类同样支持设置超时时间。
02、小心传递SpinLock实例
在传递SpinLock实例时,需要十分小心,这是因为SpinLock是结构体即为值类型,当通过值传递,会导致创建该结构体的副本,复制一个新的实例,而不是传递引用。
如下示例代码:
public class CopySpinLockExample
{
public void Method1(int thread, SpinLock lockCopy)
{
var lockTaken = false;
//尝试获取锁
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"线程 {thread},成功获取锁");
}
else
{
Console.WriteLine("线程 {thread},未获取到锁");
}
}
}
public static void CopySpinLockRun()
{
var example = new CopySpinLockExample();
SpinLock spinLock = new SpinLock();
example.Method1(1, spinLock);
example.Method1(2, spinLock);
spinLock.Exit();
Console.WriteLine("主线程,释放锁");
}
这段代码有两个问题是:
1.方法Method1的两次调用中的lockCopy是各不相同的锁,即会导致两次调用都能获取到锁;
2.方法Method1中的lockCop和主方法中spinLock是两个不同的锁,会导致主方法释放锁异常;
我们可以看看代码执行结果:

方法两次调用都成功获取锁,同时最后释放锁时抛出了异常。和我们上面说的两个问题完全一致。
而要解决这个问题也很简单,只需要把Method1方法的SpinLock参数前加上ref即可。代码如下:
public class RefCopySpinLockExample
{
public void Method1(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//尝试获取锁
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"线程 {thread},成功获取锁");
lockCopy.Exit();
Console.WriteLine($"线程 {thread},释放锁");
}
else
{
Console.WriteLine("线程 {thread},未获取到锁");
}
}
public void Method2(int thread, ref SpinLock lockCopy)
{
var lockTaken = false;
//尝试获取锁
lockCopy.Enter(ref lockTaken);
if (lockTaken)
{
Console.WriteLine($"线程 {thread},成功获取锁");
}
else
{
Console.WriteLine("线程 {thread},未获取到锁");
}
}
}
public static void RefCopySpinLockRun()
{
var example = new RefCopySpinLockExample();
SpinLock spinLock = new SpinLock();
example.Method1(1, ref spinLock);
example.Method2(2, ref spinLock);
spinLock.Exit();
Console.WriteLine("主线程,释放锁");
}
执行结果如下:

从结果上可以发现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;
具体代码如下:
public class MySpinLock
{
// 0 - 未锁定, 1 - 锁定
private volatile int _isLocked = 0;
//获取锁
public void Enter()
{
while (true)
{
//使用原子操作检查和设置锁
if (Interlocked.CompareExchange(ref _isLocked, 1, 0) == 0)
{
//成功获得锁
return;
}
}
}
//释放锁
public void Exit()
{
//释放锁,直接设置为未锁定状态
_isLocked = 0;
}
}
然后把使用示例中的代码SpinLock替换为MySpinLock即可验证我们自己的自旋锁实现,运行结果如下,基本和原生的SpinLock功能一致。

注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner
并发编程 - 线程同步(八)之自旋锁SpinLock的更多相关文章
- Python并发编程-线程同步(线程安全)
Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...
- 并发编程-线程-死锁现象-GIL全局锁-线程池
一堆锁 死锁现象 (重点) 死锁指的是某个资源被占用后,一直得不到释放,导致其他需要这个资源的线程进入阻塞状态. 产生死锁的情况 对同一把互斥锁加了多次 一个共享资源,要访问必须同时具备多把锁,但是这 ...
- 33 - 并发编程-线程同步-Event-lock
目录 1 线程同步 1.1 Event 1.1.1 什么是Flag? 1.1.2 Event原理 1.1.3 吃包子 1.2 Lock 1.2.1 lock方法 1.2.2 计数器 1.2.3 非阻塞 ...
- Python并发编程05 /死锁现象、递归锁、信号量、GIL锁、计算密集型/IO密集型效率验证、进程池/线程池
Python并发编程05 /死锁现象.递归锁.信号量.GIL锁.计算密集型/IO密集型效率验证.进程池/线程池 目录 Python并发编程05 /死锁现象.递归锁.信号量.GIL锁.计算密集型/IO密 ...
- C#并行编程-线程同步原语
菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...
- 并发编程从零开始(八)-ConcurrentHashMap
并发编程从零开始(八)-ConcurrentHashMap 5.5 ConcurrentHashMap HashMap通常的实现方式是"数组+链表",这种方式被称为"拉链 ...
- Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
- 【转】Java并发编程:同步容器
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...
- Python并发编程-线程
Python作为一种解释型语言,由于使用了全局解释锁(GIL)的原因,其代码不能同时在多核CPU上并发的运行.这也导致在Python中使用多线程编程并不能实现并发,我们得使用其他的方法在Python中 ...
- 8、Java并发编程:同步容器
Java并发编程:同步容器 为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch). ...
随机推荐
- 2019 ICPC Universidad Nacional de Colombia Programming Contest
A. Amazon 给定\(n\)条直线(存在共线的情况),在每两条垂直的直线的交点处需要建一个交叉点,求交叉点的数量,注意需要去除共线时候的交叉点 题解 因为要除去共线的情况,我们考虑将一条直线以方 ...
- m4 mac mini本地部署ComfyUI,测试Flux-dev-GGUF的workflow模型10步出图,测试AI绘图性能,基于MPS(fp16),优点是能耗小和静音
m4 mac mini已经发布了一段时间,针对这个产品,更多的是关于性价比的讨论,如果抛开各种补贴不论,价位上和以前发布的mini其实差别不大,真要论性价比,各种windows系统的mini主机的价格 ...
- Acrobat Pro DC 2024.005 像word一样编辑PDF
随着数字化的推广,PDF文件凭借其强大的优势和稳定性逐渐成为各类文档交流和存储的首选格式.随之而来的是对PDF文件的阅读.编辑.转换.转曲等各种操作需求的不断增长.因此,一款强大的PDF处理软件不仅需 ...
- PCB设计AD规则设置(按照嘉立创设置)
本文转载自https://blog.csdn.net/subtitle_/article/details/121648972 官方参考https://www.jlc.com/portal/vtechn ...
- django推导流程
目录 一.纯手撸web框架 二.基于wsgiref模块 三.代码封装优化 四.动静态网页 五.jinja2模块 六.前端.后端.数据库三者联动 一.纯手撸web框架 1.web框架的本质 理解1:连接 ...
- 【数据库】MySQL概念性基础知识期末复习
选择题 第一章 3 二维表结构--数据模型--关系数据模型 5 描述全部数据整体逻辑结构--模式 6 逻辑数据独立性--模式变,外模式和应用程序不变 7 物理数据独立性--内模式变,外模式和应用程序不 ...
- Docker封装Java环境镜像(Alpine+OpenJDK)
在给Java程序封装镜像时,使用的基础镜像动辄上百M,还需要每次部署的时候挂载时区等问题,不如自己封装一个镜像,供之后使用. 这里使用Alpine Linux(3.9) 安装OpenJDK 1.8及部 ...
- rsync+ssh同步备份文件
定期对web代码或重要的文件做同步异地服务器备份,防止服务器故障严重磁盘损坏时文件丢失的问题. 备份服务器:192.168.200.134 目标服务器:192.168.201.65 rsync同步命令 ...
- Qt音视频开发24-视频显示QOpenGLWidget方式(占用GPU)
一.前言 采用painter的方式绘制解码后的图片,方式简单易懂,巨大缺点就是占CPU,一个两个通道还好,基本上CPU很低,但是到了16个64个通道的时候,会发现CPU也是很吃紧(当然强劲的电脑配置另 ...
- 百度公共IM系统的Andriod端IM SDK组件架构设计与技术实现
本文由百度技术团队分享,引用自百度Geek说,原题"百度Android IM SDK组件能力建设及应用",本文进行了排版和内容优化. 1.引言 移动互联网时代,随着社交媒体.移动支 ...