一个无锁消息队列引发的血案:怎样做一个真正的程序员?(二)——月:自旋锁

前续

一个无锁消息队列引发的血案:怎样做一个真正的程序员?(一)——地:起因

一个无锁消息队列引发的血案:怎样做一个真正的程序员?(二)——月:自旋锁

平行时空

  在复制好上面那一行我就先停下来了,算是先占了个位置,虽然我知道大概要怎么写,不过感觉还是很乱。

  我突然想到,既然那么纠结,那么混乱,那么不知所措,我们不如换个视角。记得高中时看过的为数不多的长篇小说《穆斯林的葬礼》,作者是:霍达(女),故事描写了两个发生在不同时代、有着不同的内容却又交错扭结的爱情悲剧,一个是“玉”的故事,一个是“月”的故事。结构上采取交叉的模式,一章写“玉”,一章写“月”,分别写两代人的命运,手法新颖,别有一番风味,用电影语言来讲,就是平行蒙太奇。用在这里再好不过了,就这么决定了!

  之所以会想到这个,源自于昨天下午还没开始写第一篇后半部分之前,我本着对读者负责的态度,特意搜索了一下“自旋锁”这个关键词,虽然说不陌生,但想到从来没有通过搜索引擎认真研究过这个词,还是有些必要。搜索的结果,有用的信息的确并不多(不是说完全没用,是基本没用),但有一条还是不错的,这也给我解释自旋锁提供了良好的思路和理论依据。

  这篇文章就是《自旋锁代替互斥锁的实践》,这是一篇译文,原版的文章在这里:Practice of using spinlock instead of mutex ,说句题外话,昨天我研究 disruptor ,搜索的一些重要的文章 也是在 并发编程网(ifeve.com) ,而且 disruptor 更新了,文章也会跟着更新,虽然跟不上 disruptor 的更新速度:(

  这里解释一下,我把两个时空定义为:“地球”和“月球”,地球:喧闹而嘈杂,到处充满着竞争,这条线主要写事情的经过和free-lock(无锁编程),我偶尔也会喷一下人;月球:代表着宁静和理性,杳无人迹,也远离那些尘世的纷争,这条线主要描述自旋锁(混合自旋锁,hybrid spinlocks),同时,月亮也是一种美的象征(我本来想选火星,可是火星太不美了……而且“火星人”别有他意,你懂的)。

  好啦,我们开始吧,菠萝菠萝蜜!

时空穿梭

  你好,欢迎来到月球。

  下面我们来谈一谈自旋锁。为什么要谈自旋锁,我们之前不是在讲无锁消息队列吗?对的,可是分析了 q3.h 后,发现其实其大致相当于两个自旋锁在运作。

  至于为什么相当于两个自旋锁,我们后面再讲,先来研究一下 互斥锁(mutex locks) 和 自旋锁(spin locks)。

互斥锁与自旋锁

  互斥锁 和 自旋锁 是 多线程编程 中的重要概念。我们用它们来锁住一些共享资源,以防止并发访问这些共享数据时可能导致的数据不一致性问题。但是它们的不同之处在哪里? 我们应该在什么时候用 自旋锁 来代替 互斥锁 呢?

理论分析

  我们在使用 互斥锁 的时候,假设 互斥锁 已经被某个线程持有(锁住了),那么另一个线程在尝试加锁的时候会失败,然后进入休眠状态以等待其他线程的运行。这个线程会一直休眠到持有锁的线程释放了锁以后,才会被唤醒。那么我们来看看 自旋锁,如果一个线程在尝试持有一个 自旋锁 的时候,如果持有没有成功(即有其他线程已经先持有该锁了,已锁住了),该线程会一直尝试持有改锁(通过在用户态自旋实现的),那么它不允许其他线程在CPU的当前核心上运行(当然,操作系统可以通过中断或线程的时间片用完后强制转换到别的线程上运行)。

存在的问题

  互斥锁 存在的问题是,线程的休眠和唤醒都是相当昂贵的操作,它们需要大量的CPU指令,因此需要花费一些时间。如果 互斥量 仅仅被锁住很短的一段时间, 用来使线程休眠和唤醒线程的时间会比该线程睡眠的时间还长, 甚至有可能比不断在 自旋锁 上轮训的时间还长。而 自旋锁 的问题是,如果 自旋锁 被持有的时间过长,其它尝试获取 自旋锁 的线程会一直轮训自旋锁的状态,这将非常浪费CPU的执行时间,很关键的一点是这些浪费的轮训时间都是 无用功 ,这时候该线程睡眠会是一个更好的选择。

解决方案

  在 单核 / 单CPU 的系统上使用 自旋锁 是没有意义的,因为它就一个运行线程/核心,你占着不放,那么其他线程将得不到运行,其他线程得不到运行,这个锁不能被解锁。换句话说,在 单核 / 单CPU 系统使用 自旋锁,除了浪费点时间外没有一点好处。这时如果让这个线程(记为线程A)休眠,其他线程就得以运行,然后就可能会解锁这个 自旋锁,线程A就可能在重新被唤醒后,如愿以偿的持有锁。

  在 多核 / 多CPU 的系统上,特别是大量的线程只会短时间的持有锁的时候,在使线程睡眠和唤醒上浪费大量的时间,也许会显著降低程序的运行性能。使用 自旋锁,线程可以充分利用系统调度程序分配的时间片(经常阻塞很短的时间,不用休眠,然后马上继续它们的工作了),以达到更高的处理能力和吞吐量。

实践

  程序员往往并不能事先知道哪种方案更好,比如,不知道运行环境CPU的核心数,不能很好的预估被锁区域的持续时间。操作系统也不能分辨哪些指令是特别针对单核或多核CPU做过优化的,所以大部分操作系统不严格区分 互斥锁 和 自旋锁 。实际上,绝大多数现在操作系统采用的是 混合型互斥锁 (hybrid mutexes) 和 混合型自旋锁 (hybrid spinlocks)。 它们是什么意思呢?

混合型互斥锁

  混合型互斥锁,在多核系统上,它的表现起初跟 自旋锁 一样,如果一个线程不能获取(持有) 互斥锁,它不会马上进入休眠状态,因为互斥量可能很快就被解锁,所以这时表现得跟 自旋锁 一样。只有当尝试一段时间以后(或者一定次数,或其他指标),还不能持有改锁,它就会被切换到休眠状态。如果运行在 单核/单CPU 的系统上时,这种机制将不会自旋,也不该自旋(原因就像前面讲的,一点好处都没有)。

  最好的例子就是Windows上的临界区,有一个 API 叫 InitializeCriticalSectionAndSpinCount(mutex, dwSpinCount),该函数的 dwSpinCount 值,Windows 默认推荐是 4000 (相信很多熟悉Windows开发的人都知道),即自旋 4000 次,而这 4000 次是怎么自旋的,以何种指令的形式自旋我们不得而知,但是它地球是先尝试自旋 dwSpinCount 次后仍未能够持有 互斥量 才真正进入休眠状态。

混合型自旋锁

  混合型自旋锁,起初表现得跟正常的 自旋锁 一样,但是为了避免浪费大量的CPU时间做无用功,会有一个折中的策略。这个策略有可能不会切换到休眠状态或者尽量很晚才切换到休眠状态,先自旋一段时间,自旋多久,以何种形式自旋将是这种类型自旋锁的核心之一;此外,另一个更核心的内容是,你还可以决定是否放弃当前线程的执行(马上放弃或等一段时间再放弃,等的时间由自旋策略决定),把时间片让给别的线程,这样提高了别的线程解锁 自旋锁 的可能性(当然,线程切换导致上下文的切换甚至可能切换到别的CPU核心上从而导致缓存失效,这可能并不一定比线程进入休眠再唤醒的时间短,这个很难权衡,但是好处是其他线程得以运行,你进入休眠后让出时间片,本质上也会遇到同样的问题)。

  作者自己写的 RingQueue 里用的就是这种 混合型自旋锁,只是 混合型自旋锁 里讲究的是休眠的策略,策略不同,CPU的占用率,执行效率都有很大区别。例如,著名 Intel 的多线程并行库里的 spin_mutex(即spin_lock) 采用的是 spin_count *= 2,自旋次数依次增大两倍,而休眠策略则在 Windows 下是 SwitchToThread() (这个有很大的缺陷,以后会讲),Linux下是 sched_yiled() (Linux下使用这个合理很多,但是那个但……,也不足够好,从细节上来说,Windows 和 Linux 下都各有优劣,且都存在缺陷,后面会说到,不过这个函数的好处就是基本Linux只有这么一个接口,而不像Windows下细节太多且不全面而容易出现致命的缺陷)

  再比如 pthread 里的 pthread_spin_lock(),不过这是个不折不扣的 自旋锁,没有休眠策略。然后还有 boost 里提供的几个 spin_lock,boost/smart_ptr下面有一个,策略还是比较得当的,自旋策略正常,但是Windows下,使用了Sleep(0)和Sleep(1),而没使用SwitchToThread(),另外一个地方,好像 boost/log 下面的,休眠策略只使用了 SwithToThread(),而没有Sleep(0)和Sleep(1)。当然还有 folly 里的 SmallLocks.h,自旋策略OK,但休眠策略使用的类似Sleep(1)的形式。

  混合型自旋锁 的性能好坏直接由你将要锁住的资源的处理时间长短,以及你的 自旋锁 的具体休眠策略而决定,性能好坏可能很难界定,不过还是能给出一些相对硬性的指标,比如 总体运行时间 和 总的CPU占用率,虽然这两个指标是相互矛盾的,但是如果你的运行时间短,占用CPU的比率又低,那肯定就是更好的 自旋锁 啦,也就是我们常说的,既想牛儿吃的是草,又想牛儿挤出的是牛奶,效率上最低不能低于 互斥锁,这也是检验 混合型自旋锁 优劣的一个参考指标。

休眠

  好了,今天就写到这里,我也要开始休眠了。其实昨天晚上我研究了一晚上 disruptor,因此耽误了写这篇文章的时间。你在看上面文章内容的时候,肯定会在想,为什么要用 自旋锁 或 互斥锁,我用 disruptor 不是更好吗?通过我的理解和研究,事实上并不是那样的,disruptor 好是好,但是是有局限的,它并没有规避多线程编程的真正问题,它只是把问题简化,尽量使用单线程,或最大程序的避免缓存、总线和资源间的争用,尽量都使用单一线程来处理问题。所以它是鼓励使用 单生产者 模式的,这样就有效的规避了一些多线程争用的问题,所以我用 disruptor 尝试了 多生成者,多消费者 的模式,试验的结果跟一般的自旋锁没有太大的区别,可能线程多了,还要更慢一些,所以 disruptor 的解决方案和我们的问题基本上可以讲是两个不同的命题,不是一个维度的东西,虽然看起来好像有那么一点雷同。但是两者都可以相互借鉴,disruptor 里面也定义了很多休眠策略,可是实际的效果并不理想。关于 disruptor,以后可能花一点篇幅来讲。

RingQueue

  RingQueue 的GitHub地址是:https://github.com/shines77/RingQueue,也可以下载UTF-8编码版:https://github.com/shines77/RingQueue-utf8。 我敢说是一个不错的混合自旋锁,你可以自己去下载回来看看,支持Makefile,支持CodeBlocks, 支持Visual Studio 2008, 2010, 2013等,还支持CMake,支持Windows, MinGW, cygwin, Linux, Mac OSX等等,当然可能不支持ARM,没测试环境。

  (未完待续……敬请期待……)

时间: 2024-12-29 06:15:40

一个无锁消息队列引发的血案:怎样做一个真正的程序员?(二)——月:自旋锁的相关文章

一个无锁消息队列引发的血案(六)——RingQueue(中) 休眠的艺术 [续]

目录 (一)起因 (二)混合自旋锁 (三)q3.h 与 RingBuffer (四)RingQueue(上) 自旋锁 (五)RingQueue(中) 休眠的艺术 (六)RingQueue(中) 休眠的艺术 [续] 开篇 这是第五篇的后续,这部分的内容同时会更新和添加在 第五篇:RingQueue(中) 休眠的艺术 一文的末尾. 归纳 紧接上一篇的末尾,我们把 Windows 和 Linux 下的休眠策略归纳总结一下,如下图: 我们可以看到,Linux 下的 sched_yield() 虽然包括了

计算机程序的思维逻辑 (61) - 内存映射文件及其应用 - 实现一个简单的消息队列

本节介绍内存映射文件,内存映射文件不是Java引入的概念,而是操作系统提供的一种功能,大部分操作系统都支持. 我们先来介绍内存映射文件的基本概念,它是什么,能解决什么问题,然后我们介绍如何在Java中使用,我们会设计和实现一个简单的.持久化的.跨程序的消息队列来演示内存映射文件的应用. 基本概念 所谓内存映射文件,就是将文件映射到内存,文件对应于内存中的一个字节数组,对文件的操作变为对这个字节数组的操作,而字节数组的操作直接映射到文件上.这种映射可以是映射文件全部区域,也可以是只映射一部分区域.

Lua 下实现一个简单的消息队列

Lua 下实现一个简单的消息队列,如下简单的几条代码就可以了. local q1 = {} local q2 = {} -- 产生消息只需要 table.insert(q1, msg) -- 分发消息需要两层循环, 可以处理 dispatch 过程中产生的新消息 while q1[1] do q1,q2 = q2,q1 for i=1,#q2 do dispatch(q2[i]) q2[i] = nil end end

又一例 select for update 的悲观锁使用所引发的血案

1. 事故描述 昨日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应. 2. 硬件 LB 查看硬件负载路由情况:ARRAY-3(config)#sh stati sl r tcp JiChuYinYong_a_8001Real service JiChuYinYong_a_8001 192.168.1.137 8001 UP ACTIVE        Main health check: 192.168.1.137 8001 tcp UP        Max Conn C

关于换个jdk引发的血案,对一个新的eclipse如何下手

总结一下今天一天发生的问题..气的上火 1.先是eclipse,换了JDK后,我的ecplise不兼容JDK7,但是我去官网总是下不到对的版本,结果是进错网页,没有搞清楚哪个eclipse对应哪个版本对应哪个名字 2. 然后是maven setting.xml中 配置没有改过来,结果eclipse一update就会把所有的我设定好的配置全部还原,让人抓狂啊 (今天copy的工程也是这种情况,让人吐血,不过我配置没错呀)3. web项目修改后不会自动发布,这个也真是,把所有配置看了一遍又一遍,完全

一个ajax同步与异步引发的血案。

前言 公司做网上促销活动,需要充值换取相应的抽奖资格,抽奖可以获得丰厚的礼品,而且抽奖资格门槛有点高,领导下达命令保证活动的正常上线与运行,领导很重视,就这样,在领导的安排下进行了相关活动的coding... 正文 开发过程倒还顺利,只是转盘抽奖耽误了点时间,不过最后还是正常制作完毕,相关测试人员也进行了测试,活动上线开始倒计时,心里有那么点忐忑. 果不其然,活动上线2分钟,一个玩家在没有任何充值,而且抽奖资格也是0的情况下,抽了5,6次,都获得了比较给力的礼品,这是查看数据库记录得到的结果,由

进程-IPC 共享内存和消息队列 (三)

详见:https://github.com/ZhangzheBJUT/linux/blob/master/IPC(%E4%B8%89).md 五 共享内存 5.1. 共享内存简介 共享内存指多个进程共享同一块物理内存,它只能用于同一台机器上的两个进程之间的通信.在进程的逻辑地址空间中有一段地址范围是用来进行内存映射使用的,该段逻辑地址空间可以映射到共享的物理内存地址上(进程空间介绍:http://blog.csdn.net/zhangzhebjut/article/details/3906025

消息队列概念与认知

本文是-消息队列学习的概念与介绍篇.目的是能够对消息队列能够有一个简单的了解和大体的认知. 参考/学习资料整理(好东西要学会分享 ) B站上的黑马ActiveMQ的视频教程 Hollis公众号上的消息队列文章 架构之家公众号上的消息队列文章 JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识的文档类项目) CS-Notes(技术面试必备基础知识) JCSprout(处于萌芽阶段的 Java 核心知识库) 一个在线绘图的工具 一.消息队列简介 消息队列 MQ(message qu

EQueue - 一个纯C#写的分布式消息队列介绍2

一年前,当我第一次开发完EQueue后,写过一篇文章介绍了其整体架构,做这个框架的背景,以及架构中的所有基本概念.通过那篇文章,大家可以对EQueue有一个基本的了解.经过了1年多的完善,EQueue无论是功能上还是成熟性上都完善了不少.所以,希望再写一篇文章,介绍一下EQueue的整体架构和关键特性. EQueue架构 EQueue是一个分布式的.轻量级.高性能.具有一定可靠性,纯C#编写的消息队列,支持消费者集群消费模式. 主要包括三个部分:producer, broker, consume