写在前面

在开始正式的讨论前,我先抛出几个问题:

  • 谈到磁盘时,常说的HDD磁盘和SSD磁盘最大的区别是什么?这些差异会影响我们的系统设计吗?
  • 单线程写文件有点慢,那多开几个线程一起写是不是可以加速呢?
  • write(2)函数成功返回了,数据就已经成功写入磁盘了吗?此时设备断电会有影响吗?会丢失数据吗?
  • write(2)调用是原子的吗?多线程写文件是否要对文件加锁?有没有例外,比如O_APPEND方式?
  • 坊间传闻,mmap(2)的方式读文件比传统的方式要快,因为少一次拷贝。真是这样吗?为什么少一次拷贝?

如果你觉得这些问题都很简单,都能很明确的回答上来。那么很遗憾这篇文章不是为你准备的,你可以关掉网页去做其他更有意义的事情了。如果你觉得无法明确的回答这些问题,那么就耐心地读完这篇文章,相信不会浪费你的时间。受限于个人时间和文章篇幅,部分议题如果我不能给出更好的解释或者已有专业和严谨的资料,就只会给出相关的参考文献的链接,请读者自行参阅。

言归正传,我们的讨论从存储器的层次结构开始。

Read More

突然想聊聊这个话题,是因为知乎上的一个问题多次出现在了我的Timeline里:请问,多个线程可以读一个变量,只有一个线程可以对这个变量进行写,到底要不要加锁?可惜的是很多高票答案语焉不详,甚至有所错漏。所以我想在这篇文章里斗胆聊聊这个水挺深的问题。受限于个人水平,文章若有错漏,还望读者不吝赐教

首先约定,由于CPU的架构和设计浩如烟海,本文站在工程师的角度,只谈IA32/AMD64(x86-64)架构,不讨论其他架构的细节和差异。并且文章中主要引用Intel的文档予以佐证,不关注AMD在实现细节上的差异。

众所周知,当一个执行中的程序的数据被多个执行流并发访问的时候,就会涉及到同步(Synchronization)的问题。同步的目的是保证不同执行流对共享数据并发操作的一致性。早在单核时代,使用锁或者原子变量就很容易达成这一目的。甚至因为CPU的一些访存特性,对某些内存对齐数据的读或写也具有原子的特性。

比如,在《Intel® 64 and IA-32 Architectures Software Developer’s Manual》的第三卷System Programming Guide的Chapter 8 Multiple-Processor Management里,就给出了这样的说明:

Read More

背景

很久以前写过一篇调试内存错误的文章《用gdb配合内核转储文件瞬间定位段错误》,这是最基本的C/C++程序内存错误的定位方法。简单的coredump问题一般都是普通的内存访问错误,通过这个方法都很容易定位到相关的代码bug并修复。

但是在具体实践中,往往会有一些复杂的内存访问错误,尤其是多线程环境下的C/C++程序。因为其内存的分配、释放与访问经常会牵扯到多个线程,容易引入复杂而难以定位的内存错误,导致程序在执行过程中错误的访问内存而被操作系统结束掉。

这篇文章介绍一些常见的内存错误和调试的步骤和方法,以及一些多线程程序避免内存问题的实践经验。

常见的内存错误举例

C/C++程序被称之为系统编程语言,往往编译成操作系统直接支持的可执行文件格式。C/C++语言本身没有垃圾回收机制,内存的动态分配与释放需要程序自行控制,对内存的访问也没有语言级别的校验和保护。出现内存访问错误后,进程多半会直接被操作系统结束掉。少部分情况因为访存地址合法,会对数据造成破坏(悬垂指针或者野指针),一般会在运行一段时间后才因为异常退出。这时候触发错误导致进程退出的代码位置往往不是”案发的第一现场“,给调试工作带来了更大的难度。

Read More

背景介绍

这篇文章主要针对C++11标准发布之后的现代C++的并发编程进行阐述。C++11首次在语言层面承认了多线程的存在,这使得“仅仅使用C++标准库就能编写跨平台的多线程程序”的愿望成为现实。

设计多线程的程序目的主要有两个:充分利用多核CPU的性能(利用多核心的计算能力以及让计算和IO重叠来降低RT并提升吞吐量)和简化程序逻辑(即把单线程状态机的逻辑拆分成多个线程彼此同步,这么做虽然不见得能提升代码性能,但是可以简化代码逻辑)。然而有些场合是不合适使用多线程的,最常见的两种场景是:限制CPU使用率线程代码中调用了fork(2)。前者不用解释,后者道理也很简单,因为fork(2)这个系统调用只会复制当前调用了该系统调用的线程,而其它线程并不会原样复制。如果此线程执行路径上的某个互斥锁已被没有原样复制的线程持有,那么该线程将永远死锁。只复制当前线程的行为也很合理。因为其它线程可能等在IO上,可能持有某些互斥锁,这些都使得forkall这样的行为难以实现。除非立即在调用fork(2)后调用exec(2),否则在线程代码里调用fork(2)可不是什么好主意。

其他的非必要场景也可以举一个例子,比如少量的CPU负载就能把IO跑满(静态web或者文件下载服务器)。这样的场景没有必要使用多线程,因为增加线程数也没有办法提高吞吐量。

Read More