Archive for 2010年10月

了解背景

2010/10/24

同事向我介绍张银奎的译作《观止》( Showstoper )很久了。可我一直都提不起兴趣来读这段克隆 VMS 的历史。因为非常赞同《 The Art of UNIX Programming 》的观点,所以 NT 内核这个工程从来在我眼里属于二流(和 Linux kernel 之类相比)。我要承认 David Cutler 从精力、智力、意志力、对软件业的价值等等方面比我强不止两三倍甚至十几倍。但我认为,一个人如果要让别人花时间读自己的故事,那么比别人强百倍也并非过分的要求。同样,我只是说强百倍并非过分,而不是说那些比平常人优秀一些的人的故事就不值得看。

最近突然想在闲暇的时候看看软件史的八卦,所以借来读了读。开始阅读之前我这样想,NT 比 Unix 在底层的清晰和稳固方面是差了一些,但是它仍然是一个成功的产品,其弱点可能更多的应该归咎于历史机缘,让我来看看它的诞生有什么激动人心的地方。

读过三分之一之后,我又退回到最初的想法,David Culter 仍旧比我阅读前所期望的要差。感觉从『 Cutler 没能借鉴 Unix ,但是他仍然做了一个伟大的产品』变成了『 Cutler 把 VMS 带到 Microsoft,让 Microsoft 勉强不至于在 Unix 之前没有还手之力』或者说『让 Microsoft 在 Unix 商业阵营的疏忽面前不至于放过机会』。这主要因为两点:

第一是 Culter 只负责 NT 内核。而大多数人都应该同意 NT 能取得商业成功的亮点绝非内核。《观止》在序言里就说道『 NT 4 改写了网络工程师的模型:不需要再记住一长串文字命令,系统是可以用鼠标管理的』。

你可以为 Cutler 辩护,内核是 NT 图形的基础。在当年的 386 硬件上完成如此基础,才是他的功绩。而这正是我疑惑的第二点,NT 内核的构建是在 90 年代初,花费了一个百人团队 5 年时间,而与此几乎同期的 Linux kernel 的雏形由个人完成。正如《 Art of UNIX Programming 》所言,NT 的成功来自商业 Unix 社区的分裂和他们对 PC 的忽视,而不是技术上的难度。Culter 成就了历史,但是并不是在逆境中通过过人(仍然强调,『过人』的『人』不是我这样的人)的能力把资源极致优化的结果,相反,我的感觉是他拥有远远超过必要的资源,而只是勉强把事情做成。如果当年其它阵营向 PC 服务器领域正面进攻,NT 的技术水平是无法招架的。这是 Microsoft 和 Gates 的成功,而 Culter 的成功更多的在于找到了 Microsoft 这个有财力和意愿把他的能力倍增的组织,却并非释放了自身的潜能。

所以,没有什么对原有看法的颠覆。对 VMS 的克隆仍然更像一段技术债务而非激动人心的革命。

不过,看到 Cutler 和 NT 团队的人以写代码为本能,我开始逼着自己在晚饭后从懒洋洋的只想看看业界八卦的情绪中挣脱出来,写写大段的代码。再次发现,只要不停的写下去,软件的乐趣其实很简单。这是这段阅读给我的回报。

我在网上,虽然没有上网

2010/10/08

父母在郊外的新家刚刚重新装修好,还没有安装 ADSL 宽带。国庆要陪他们住几天,所以赶紧办了一个联通的 WCDMA 号码,以便用 3G tether 上网。同事看到我新办的号码,知道原由之后对我说『你就不能利用国庆假期休息休息戒戒网瘾?』

仔细想想,我是有轻度网瘾吧。可是还没有那么严重。这个假期我想好好读读朋友给我推荐的 alternative history 小说(怎么翻译呢?叫历史虚构小说如何)。偏巧这本书属于国内不大可能出版的那种 [1]。好在 Amazon 支持跨洋送货,等待了十几天之后,终于在国庆长假前送到我手上。

以我的英语水平用阅读中文小说一半的速度阅读没有问题,但词典是不能离手的。而且,今天网络词典和本地词库相比,前者中的中等水平产品对俚语和短语的解释也要比后者中的高级别专业产品好。所以我必须联网。

网络词典的需求仅仅是一部分原因,而且将就的话还勉强可以用 iPhone 来替代 [2]。这本小说讲述在柏林发生的故事,引用了大量二战前后的历史人物、历史事件(比如 Night and Fog)、各大洲地名,以及德语的专有名词(比如 Obergruppenführer)。即使是美国读者若非对欧洲和二战很感兴趣读起来也会颇为吃力。我必须要时刻访问 Google 和 Wikipedia 这样的资料。对于查阅资料的任务来说,iPhone 的屏幕大小或者 GPRS/EDGE 的网速太吃力了。

所以,我必须待在网上,即使没有上网。互联网不是消磨时光的好去处,但是这项伟大的技术让人能够而且敢于在没有背景知识的情况下去欣赏不熟悉的作品。时刻联网也许会削弱我们的智力,但是需要重建的是对其它事物的兴趣而非拒绝网络。

注释:

  1. 也许压根 alternative history 这类小说都没有在国内出版的可能。
  2. iPhone 可以用 GPRS 上网,但是不能用 GPRS tether 上网。Nokia 的手机倒是可以,不过那是另外一个话题。

多核与锁

2010/10/04

业界有两次事件刺激了关于多线程在桌面开发中的应用和相关讨论:Windows 95 将抢占式多线程(preemptive threading)引入主流桌面平台;AMD 和 Intel 把多核处理器引入桌面。前者让桌面的主流开发者开始关心锁(lock、mutex [1])的使用。后者是硬件设计者无计可施的体现 —— 无法进一步提高时钟频率,也无法进一步提高硬件自动对代码的执行进行并行化处理的程度,关于这方面的评论 Herb Sutter 的《不再有免费的午餐》最为精彩,他用了『撞上物理定律铁壁』(reach hard physical limits)这样的描述。

写过《不再有免费的午餐》之后 Herb Sutter 又对锁的应用写了一系列文章(123[2]。在他看来这是对《不再有免费的午餐》一文的自然延续。可能很多人也同意这点。但我认为这后三篇文章和《不再有免费的午餐》讨论的是两个不同问题,也就是说,锁和多核 —— 虽然它们有必然的协作需求 —— 但是主要是为了应对不同的问题而出现的。把它们分别要解决的两个问题在同一个主题下讨论是并行软件开发领域很多讨论混乱的根源。

计算的本质

什么是计算的本质,关于这个问题计算机科学家会告诉你关于图灵机、寻径问题、判定性问题等等概念。而我只想找一个让普通开发者看的舒服的说法。基本上,计算是把一种形式的数据变换成为另一种形式。

硬件制造者试图通过两种方式提高执行『算法』的效率 [3] :提高 CPU 的时钟频率;试图让硬件自动找到算法中没有相互依赖的部分并且最大可能的并行执行它们。前者遇到了『物理定律铁壁』。后者主要通过乱序执行(out-of-order)和超标量流水线(super-scalar pipeline)实现,也已经接近优化的极限。

并行执行

所以,硬件制造者决定把第二种优化方式的控制权直接丢给软件开发者,而不再试图用硬件自动寻找可以并发的部分。这就是『免费午餐』的终结。

这种『并发执行』的关键是如何把数据分割成在算法处理中互不依赖的,而在处理之后又能重新汇合成完整数据的片段。这个过程中除了在最后的数据合并阶段需要少量的同步操作,关键在于数据分割的方法。锁在这个模式中并没有非常重要的作用。

其实把『并发执行』的控制权 —— 也就是数据分割的责任 —— 丢给软件开发者并非始于多核,从 MMX 时代开始流行的 SIMD 指令就是一个例子。

并发访问

锁的重要性,正如上文所述,其实和业界第二次刺激多线程开发的原因 —— 多核并没有绝对的联系。多核是硬件制造者将 out-of-order 和超标量的等效作用向上转移到软件设计的决定。其关键在于数据的无依赖分割。

而第一次刺激多线程开发的因由,Windows 95 将 preemptive threading 引入主流桌面开发,才是锁占据主导地位的事件。这次事件远在多 CPU 引入桌面系统之前。这时多线程的应用并非为了提高算法的执行效率,而是为了改善用户界面的响应速度。

如果从『数据 A』到『数据 B』的处理时间非常久,让用户能够稍感满意的做法是不时的显示处理的中间状态。这需要把算法分割成一系列步骤,把每个步骤产生的中间结果交给负责显示的代码处理。这种方式中,负责数据处理的代码和负责(中间)结果显示的代码处于不同线程。锁对于两个线程的协作起了至关重要的作用,因为整个算法只有某几个特定点才能产生对用户有意义的显示(保证数据 integrity)。这个方式和上文的『并行方式』有两个区别。

第一,这个方式的关键是算法分割而不是数据分割。『并行执行』注重数据分割,分割的要求是对算法处理不会产生相互依赖。『并发访问』注重算法分割,分割的要求是数据的呈现对用户有意义。

第二,『并发访问』并不关心访问的确定性。如果显示代码显示『数据格式 A1』的速度比较慢,它完全可以忽略『格式 A2』,只要最后能正确显示『数据 B』。如果有多个显示代码线程要同时显示中间结果和最终结果,它们显示了哪些中间结果,显示中间结果的前后顺序,是完全没有关系的,只要保证每个线程中前期步骤的结果不会在后续步骤的结果之后显示,并且最终都显示最终结果就行。

执行 vs. 访问

所以,多核的目的是『并行执行』,而锁的作用在于『并发访问』。有时实际问题需要混合两种方式的方案。例如,如果数据分割不能做到完全的消除依赖,那么各个算法线程之间就会进行少量的并发访问。但是,把两种方式完全混合的方式并不多,实际的系统总是非常偏向某个方式。

有一点点讽刺的是多核和锁似乎都没有真正成为解决原本期待用它们来解决的问题的利器。适合『并行执行』的任务往往符合两种情况:一是分割后的数据相互关联程度很低,完全没有必要通过共享地址空间来协作,这时其实通过简单的多进程处理就能提高 through-put 。常见的并行编译即属于此类。另一种情况是在处理每个数据片段的时候较少用到分支,这正是向量处理和 SIMD 的强项。所以『并行执行』的任务实际更多的被 Cuda 或者 OpenCL 之类的通用 GPU 计算担当。对多核的利用应该从对锁的注意,迁移到对数据分割的理论研究。

需要『并发访问』的问题,其实在单线程和协作式多任务处理中就通过消息循环定期发送 timer 消息得到了解决。被分割后的算法放到一系列 timer handler 中和负责显示中间结果的代码在同一个线程里交替运行。使用 timer handler 过多的一个并不严重的缺点是代码的静态表现不能直观的体现其静态流程,出现问题的时候 call-stack 也不够明晰,对调试和查找 bug 增加了一定的困难。把『并发访问』的问题由 timer 模式迁移到多线程加锁的模式,虽然提高了算法线程的代码流畅程度,也由于同步操作的设置极大的增加了整体复杂度。所以大量的开发实际还是停留在协作式的方式,比如 Cocoa 开发中的动画和拖放就提倡使用 timer 来完成。在 ActionScript 这类没有线程模型的运行环境 timer 也是唯一的途径。

多核和锁都和多线程都紧密的联系,但是这种联系更多的是表面层次的。本质上它们倾向于解决各自的问题。而在分析这些多核和锁『各自的问题』时一旦很好的消除了另一种方式的干扰因素,问题本身又可以通过其它方式得到解决。

注释:

  1. 线程同步操作有很多种。Mutex 只是多线程同步化操作的一种(一般在 UNIX 中 mutex 指那些利用内核等待队列让暂时不能取得执行临界区(critical region)的线程放弃 CPU 的操作)。此外还有 bus lock,spin lock,memory barrier 等等操作。本文在不必涉及具体区别的地方统一称为锁。
  2. Sutter 的后两篇名为 lock-free 。但是正如注释 [1] 所说,这里还是需要用到 bus lock 或者 memory barrier 一类的同步操作。
  3. 第三种方式是通过高速片内缓存(on-die cache)。这是利用的算法执行对数据和代码局部化(locality)特性。因为和算法本身的逻辑执行效率没有关系,所以不做讨论。