基于 semaphore 实现轻量级 mutex

核心是 Jeff Preshing 大牛的两篇文章

讲述如何利用操作系统(Windows)提供的基础同步原语,例如 semaphore,实现一个轻量级的 mutex 同步原语。

这个 home-made mutex 有如下特点:

  • 支持常规的 lock(), unkock()try_lock()
  • 在没有 race 的情况下 lock/unlock 操作都是简单的 atomic operations,开销小
  • 支持 recursive mode(第二篇文章里阐述)

之所以说是轻量级,是因为这个实现缺少一些高级功能,例如 CRITICAL_SECTION 提供的多核环境下阻塞线程前会进入 spinning state;同时在 Linux 等系统下,缺少应对 thread priority inversion 的能力 .etc

在功能够用的情况下,根据 Jeff Preshing 给出的 benchmark,这个 Lightweight mutex 性能不俗。

前方大量评注(吐槽)

如果你看完了这两篇文章并且也大致上弄明白了文章里给出的实现代码,可以继续往下看。

0x00 事实上,Windows 的 CRITICAL_SECTION 也是类似的实现。当然附属功能多了不少就是。

0x01 第二篇文章里为了实现 recursive mode,给每个 mutex 增加了一个 owner-thread 的成员。实际上,哪怕是非 recursive 的实现都应该有这个成员:因为 mutex 区别于单纯的 semaphore 的一个核心特点是,它具有 ownership 的概念。

因此第一篇文章里的实现是有欠缺的,会导致线程 A 上的 lock 能被线程 B unlock。

这个行为直接暴露了底层 semaphore 的语义,但不是 mutex 自身的语义。

0x02 Golang 的 Mutex 实现上没有 ownership 的概念,选择了直接暴露了底层的 semaphore 语义。这是一个很迷的设计,GitHub 上有人提了 issue,但是官方给的回复非常的 interesting。

0x03 示例代码里,原子的增/减用的是 _InterlockedIncrement()_InterlockedDecrement();这两个 intrinsics 会返回更新后的值。所以相比下,使用 _InterlockedExchangeAdd() 会更加高效。

因为这三个 intrinsic 函数编译后同样生成 lock xadd 指令,但是后者返回的是 original value 指令相比下少一条。

0x04 try_lock() 的实现需要用到 CAS 指令,开销更大,因此在 recursive mode 的实现中,仅当当前线程不是 owning thread 时再去尝试改变 mutex 状态是一个更好的实现。

Anvil -- An Assistant For You CMake

0x00

前段时间专门抽空做了一个小工具,也就是这里要讲的主题:anvil

一开始做 anvil 的动力很简单:某次尝试体验一下 Linux SignalFD 功能时想直接使用 ezio 的 EventLoop 作为基础事件循环,同时项目使用 cmake 管理。

为了省事,我直接从 ezio 的项目里抠出来 CMakeLists.txt 和几个自己写的 .cmake 文件,就地修改

但事实证明哪怕这样,改动量也不小,原因大体是因为:cmake 里(非函数内定义的)变量作用域是全局的,通过 fetch-content 功能引入的依赖在 add_subdirectory() 后的模块里也能看到上一层定义的变量,因此为了防止一些控制型变量发生冲突,我都在前面加上了对应的模块前缀。

所以我面对的就是一大坨变量名的更换,以及少部分声明/属性的调整。

考虑到大部分的文件内容都是可以模板化的,而手动“实例化”不仅费事还很容易出错,所以我就很自然地萌生了写一个工具自动化这个过程的想法。

0x01

在经过一两天的短暂思考后,我大致确定了这个工具的定位和需要实现的基本目标,总结起来有三个核心点:

  1. 辅助 cmake 而不是试图替代它或深度封装
  2. 内建一个轻量型的依赖管理功能
  3. 配置化的生成 & 构建流程

首先,第一点是重中之重。

Monthly Read Posts in Apr 2019

Programming Languages

Destructors that throw

C++ 11之后的 destructor 默认是 noexcept,如果有 active exception 逃逸出 dtor 会直接触发 std::terminate(),即使外部有 catch handler。可以用 noexcept(false) 显式关闭。

因为 stack unwinding 是可以嵌套的,一个精心设计的场合下(见文中例子),可以做到多个 active exception 不在一个层次里,因此也不会触发 double-exception situation。


Who calls std::terminate?

an exception leaves out from main function

an exception leaves out from initial function of a thread

an exception leaves out from dtor (since c++ 11 with noexcept guarantee)

Write Your Own DNS Query

大部分人应该都知道 DNS 协议,以及它的用处。但是考虑过自己动手写 DNS message transmission 的应该不占多数。

我们不妨考虑如何自己实现一个简单的 DNS client,完成基本的 domain name query and response parsing。

因为 DNS 协议通常使用 UDP 作为其传输层协议,而且数据包是二进制包,所以实现一个这样简单的 demo 考虑用 golang 可能会省事儿不少。

DNS Message Format

大体上 DNS 不是一个复杂的协议(虽然各类坑实在不少),所有的消息,不管是 query 还是 reply,都共享一个消息格式:

1
2
3
4
5
6
7
8
9
10
11
+---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | Resource Records (RRs) answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
  • Header:消息头,包含一些参数、标志位
  • Question:包含 query 的信息,例如需要查询的 domain
  • Answer:reply 的信息
  • Authority:如果有信息,则表明应答的服务器是 ultimate authority server。别忘了 DNS 服务器是树形结构,通常终端用户查询的 DNS 服务器都是 local DNS server。
  • Additional:服务器传回的一些额外数据,非用户显式需要的

对于一个 query message,Answer section 是空的;而对于一个 reply message,Question 保存着对应 query 的数据。

一般而言,终端设备收到的 reply 消息里,AuthorityAdditional 是空的。所以下面会跳过这两个 section 的描述。

统一 ezio Buffer 的算术类型读写接口

ezio Buffer 一开始的时候只为 read, write 和 peek 提供了从 int8_tint64_t 的函数重载,如果需要处理 unsigned integers,那么就需要自己额外做 static_cast

ezio 的主要客户藏心同学早前抱怨过这个问题,并且同时建议我加上对 float/double 的浮点数支持。

对于这个建议我一开始是抵触的:

  • 自己 cast 又不是不能用,额外加重载支持三个操作工作量都要翻一番呢
  • 浮点数的 binary serialization 本来就是很难跨平台的,不是每个环境都(虽然大部分)要求使用 IEEE 754 spec。如果真的需要直接把浮点数存到网络包里,自己直接操作 underlying binary layout 不就好了…

于是藏心同学一开始开的 issue 我一直没理他,于是最后他自己关掉了…

等到我自己动手写一个 socks4a proxy 的时候我发现,自己 cast 真的是…太蛋疼了…而且代码看上去还非常丑,大面积的 static_cast 制造了相当一部分内容噪音。那会儿我大概有点理解藏心同学的内心感受。

于是我思考良久,打算改造 Buffer 的这部分接口,以支持绝大多数 integer types,顺带也增加入 floating piont types 的支持,这样 read, write, peek 就基本支持了绝大多数 arithmetic types。

通过直接增加重载是我极力避免的,因为除了接口签名外,大部分实现几乎是一样的,不外乎:

  • 如果是单字节,直接写/读操作
  • 如果是多字节,首先字节序转换,然后做写/读操作
  • 如果是浮点数,首先按照对应字节大小的整数类型解释内存,然后参考普通整数的处理

于是自然而然的想到直接将函数做成 function templates 来增强语义。

Monthly Read Posts in Mar 2019

Networking

The State of Browser Caching, Revisited

各家浏览器对 HTTP caching 的支持概况


Cross-Origin Resource Sharing (CORS)

ajax跨域请求及相关

和 CORS 有关的两篇 posts。第一篇是 MDN 的官方教程。

需要注意的是,CORS在服务端的表现不是限制某些 URL 只能被某些 domain 的请求访问,而是允许这些 URL 可以在满足要求的条件下(origin 是某个匹配 domain),允许接收跨域请求。

跨域禁止的同源策略是由 browser 实施的,当收到服务端许可的情况下才会继续。

SRE

Docker 核心技术与实现原理

科普型,内容深度并不深,适合作为了解

Software Engineering

What Are The Best Software Engineering Principles?

列举了一些软件工程的基本原则。


Monorepos: Please don’t!

这篇文章算是很标准的檄文:首先点出几个 monorepo 的理论优点然后逐一分析,得出这些优点并没没有看上去那么好;接着列举缺点。

最后总结得出核心是工程师文化。


Monorepo: please do!

这篇文章是上面那篇的 argue,但是相比起来写的就太一般了。

全片的核心论点就一个: monorepo 解决的是大团队里各个子团队的沟通提问题。


Things You Should Never Do, Part I

KEY: Do not rewrite your code.

Concurrency

Spurious wake-ups in Win32 condition variables

简要解释了 Windows 上 condition variables 会出现 spurious wakeups 的两个原因


Locks Aren’t Slow; Lock Contention Is

频繁的锁竞争是使用锁慢的根本原因,剩下的就老论点:减少临界区大小,减少锁竞争发生概率 .etc

文中用泊松分布模拟了实际使用 case 中锁竞争概率和性能的关系,50%的竞争下,两个线程也能由1.5x的提速

socks4a 协议代理

对于网络编程学习而言,尝试实现一个 proxy 是一个很好的途径。因为一个 proxy 对于 clients 来说是 server,对 remote servers 来说又是 client,一下就覆盖到了网络编程的两个大块。

同时,需要管理/协调两侧的连接对如何正确的处理网络连接的关闭也是一个挑战。

对比普通的 HTTP Proxy,socks4a proxy 作为 transport layer proxy 在协议自身上更加”干净“(不需要像 HTTP Proxy 那样解析厚重的各种 header 以及对 HTTPS 的特殊处理),也更贴近 TCP 网络编程的具体实践。

对比更新的 socks5,socks4a 仅支持 TCP,对 TLS 信道加密没有协议上的要求,更容易实现。

另,socks4a 协议是 socks4 的一个补丁版,两个协议的 RFC 文本打印下来整体不超过两页 A4 纸,半个小时就能看完。

上周的时候我为 ezio 增加了一个 socks4a 的简单实现,过程中发现了 TCPClient 在 teardown 时存在缺陷,有概率导致连接的断开和 TCPClient 的析构出现 race。

同时还发现作为测试的 curl v7.46 (ubuntu 16.04 LTS apt 版本)在处理 HTTPS 请求时存在缺陷,会导致 client connection 过早断开,proxy 出现 RST 错误。

这说明拿 socks4a proxy 作为熟悉具体的语言/网络库的编程范式的例子还是非常不错的。

注:socks4a 协议有两个连接模式:常用的 CONNECTBIND,后者是为了适应 FTP 的 active mode 而存在的,现在基本没有使用的必要,可以直接忽略。