27

文 / 王越

经过6年时间,4个发行版,苹果终于完成了向64位的迁移,并随着Snow Leopard的发布推出了解决并行编程问题的Grand Central Dispatch(简称GCD)技术,释放了多核系统的潜力。

和10.5一样,在10.6 Snow Leopard中,苹果继续利用64位的迁移砍掉了诸多老技术,很多新技术仅以64位的模式被支持。例如重写的QuickTime X框架,虽然QuickTime X应用程序以32位和64位的模式发布,但其API仅暴露给64位。另一个例子是Objective-C 2.1的运行库,快速Vtable调度,新的和C++统一的异常处理模型,以及彻底解决对象的FBI问题等,都仅限64位程序使用。

内核的64位化

读者应该发现,经过这4个发行版,Mac OS X自下而上地对整个系统向64位迁移。10.3内核空间提供了64位整数运算的支持。10.4允许程序以64位模式运行在用户空间,并且提供了64位的libSystem使得开发者可以开发64位的Unix程序,而10.5中系统所有未废弃的函数库、框架都提供64位版本,到了10.6,所有用户空间的程序,包括Unix层和图型界面层,基本都更新到64位。细心的读者不禁会问—那内核是64位的吗?是的,自下而上支持64位后,10.6又从上往下,迁移了整个系统中最后一个也是最重要的部分—内核。

内核64位化的意义

对于Windows、Linux,以及FreeBSD等操作系统,64位实现的第一步是实现64位的内核。然而Mac OS X却反其道而行。主要原因是,反正32位的内核也能以非模拟、非兼容的方式原生地运行64位用户空间程序,而内核和与内核动态链接的驱动,很少需要用到64位的寻址空间(你什么时候见过内核本身使用4GB内存?),所以该问题可以暂缓。

但要记住,用户空间的内存是由内核管理的,虚拟内存、内存分页等机制,都是由内核一一实现的。一旦在不久的将来,随着用户空间的内存占用越来越多,虚拟内存的分页比也会不断膨胀。比方说,一个用户程序使用4GB的空间,每个分页包含4KB的页面,那么总共有1M个页面。因此,假设一个页面需要64B的PTE来记录该页的位置,那总共也就需要64MB的内核空间来记录这个用户空间程序的虚拟内存,不算太多。而在不久的将来,如果一个64位用户程序使用128GB的空间,则需要32M个页面,每个页面64B的PTE会导致2GB的内核地址空间来寻址(暂不考虑大分页)。32位的内核就显得非常紧张。

另外,上一期我们也提到64位的Intel架构提供了比32位多一倍的寄存器,因此,用户空间程序对64位内核的系统调用也会更快。根据苹果的数据,系统调用的响应速度比原先快了250%,而用户空间和内核空间的数据交换也快了70%,因此,64位内核要比32位内核更快。

内核完成64位迁移

虽然在Mac OS X 10.6中,苹果提供了64位模式运行的内核,但在大部分苹果计算机上,这个特性并不默认启用。其原因是,虽然64位程序和32位程序可以在计算机上同时运行,但64位的程序只可以加载64位的库或插件,32位程序只能加载32位的库或插件。因此,如果默认使用64位模式启动,则诸多第三方的32位驱动或内核模块将无法使用。当然,用户可以通过修改com.apple.Boot.plist、nvram,或开机按住6和4强制加载64位内核,不过苹果并不推荐这样的方式。直到Mac OS X 10.7时,第三方内核扩展已趋完善,大部分的Mac才默认使用64位内核模式启动。

苹果用了整整6年的时间完成64位的迁移,在2009年WWDC的一个讲座上,Bertrand Serlet告诉开发者,我们这个64位技术的讲座,只针对Mac OS X,而iPhone、iPad等iOS设备,由于使用ARM平台,在可预见的未来可能并不会支持64位技术。

不过两年之后的2011年10月27日,ARMv8发布,ARM正式宣布支持64位。未来会不会出现基于ARM的Mac,或是64位的iPad,除了苹果,谁知道呢?

Bertrand Serlet在WWDC 2009上介绍Snow Leopard的64位和Grand Central Dispatch技术

GCD来临

很长一段时间以来,处理器靠更快的运行时钟来获得更高的效率。软件开发者无需改动或重新编译他们的代码,就能得到摩尔定律许诺他们的好处,因为处理器顺序地执行计算机指令,新一代的处理器就自动会跑得比原先更快。后来每每达到一个技术极限时,总有一些聪明的方法绕过这些极限,比如超纯量、指令管线化、快取等,不是悄无声息地把多条互相独立的指令同时运行,就是隐藏掉数据读写的延时。

GCD出现的缘由

到了21世纪,能想的办法基本都想尽了—现代处理器已经足够并行了,也采取了各项优化来不断提升各种预测器的准确率,而时钟频率却是不能无限提高的—提高时钟频率会极大地增加处理器的产热,使得服务器机房或笔记本的散热成为一个头痛的问题。同时对于便携设备而言,高频也意味着短得多的电池时间,因此摩尔定律正在经受重大的考验。

因此大约在21世纪头十年过掉一半时,“多核”处理器,终于开始跃入普通消费者的视线。“多核”顾名思义,就是把原先单核的半导体线路复制多份排于同一裸片上,每个核相互独立,又能彼此通信。多核处理器的出现,有效缓解了计算机处理器生产商的设计和制造压力,从而达到忽悠消费者买更新款产品这一不可告人的目的。

但这一次技术革新,并不如之前那么顺利,因为程序并不会自动在多核系统上跑得更快,甚至有很多程序每一步都有前后依赖,不能高效地并行运行。即使能够高效并行的程序,也需要大规模改写才能充分利用多核所带来的优势。

传统的并发编程模式,就是学习使用线程和锁。这听起来很简单,几句话能说明白:

把每个任务独立成一个线程;

不允许两个线程同时改动某个变量,因此得把变量“锁”起来;

手动管理线程的先后并发顺序和并发数量,让它们均匀地占满系统资源;

最好系统中只有这个程序在运行,否则你精心设计好的线程管理算法往往不能达到原来该有的效果;

最后祈祷程序在用户那儿不出问题。

但是实际操作起来,多线程程序的编写要比单线程难上不止一个数量级。一方面,调用大量内存和数据反复的加解锁本身效率就非常低下;另一个重要原因在于,由于多线程程序可能以任意的次序交错执行,程序再也无法像顺序执行时那样产生确定的结果。多线程程序看似容易编写,但难分析、难调试,更容易出错。即使是最熟练的开发者,在茫茫线程和锁之间,也会迷失方向。且程序的错误在很多时候甚至是不可重现的。所以,程序员使用线程和锁机制编写并行程序的代价是很高的。

GCD就是在这种背景下被苹果提出来的。2008年最初提出但未公布细节时,很多人怀疑它是FreeBSD的ULE调度器在Mac OS X上的实现。ULE是FreeBSD当时最新的内核调度器,用来替换掉老一代的4BSD调度器,当时使FreeBSD上跑多线程程序的效率获得了重大的性能提高,远高于同期Linux和Solaris的算法效率。但当时我就认为GCD依赖FreeBSD这项技术的可能性不大,因为Mac OS X中管理进程和线程主要用的是Mach而不是BSD。不过后来证实我只猜对了一半,GCD的实现,实际上是依赖于FreeBSD的另一项技术kqueue。kqueue是一个由FreeBSD 4时代引入的新功能,内核级别地支持消息通信管理。GCD的队列,其实就是用kqueue实现的。

GCD出现的意义

在GCD中,开发者不再管理和创建线程,而是将要实现的运算抽象成一个个任务,一起扔给操作系统,转而让操作系统管理,这在计算机科学中,被称为线程池管理模式

在GCD中,开发者使用很简单的方式就能描述清应用程序所需执行的任务,以及任务之间的相互关联。每一个任务在代码中被描述成块(block),然后开发者把一个一个块显式地按顺序扔到队列(queue)中。使用块和队列两个抽象的表述,开发者无须创建线程,也无须管理线程,更无须考虑数据的加解锁。换之而来的,是更简短可读的代码。剩下的事,全都扔给操作系统去完成。

在操作系统那边,GCD在程序运行时,管理着一定数量的线程,线程的数量是自动分配的,取决于用户计算机的配置和用户程序运行时的负载。多核工作站每个程序配到的线程,自然就会比单核手机或双核笔记本来得多。而且这个线程的数量是会动态变化的。当程序非常忙时,线程数会相应增多,而当程序闲置时,系统会自动减少其线程数量。然后,GCD会一一从队列中读入需要执行的块,然后扔到线程上并发执行。

相信读者已经看出GCD和传统线程—锁机制的区别来了。传统的方式按劳分配,强调程序自由独立地管理,妄想通过“无形的手”把系统资源平均分配,走的是资本主义市场经济的道路。而GCD按需分配,真正实现了社会主义计划经济管理模式。因此在政治上GCD就是一个代表先进生产力的计算机技术(我被自己雷了,但事实就是这样)。

GCD是一个自底向上的技术,它实际上由以下6个部分组成。

编译器层面,LLVM为C、Objective-C和C++提供了块语法,这个内容等下会介绍。

运行库方面,有一个高效分配管理线程的运行库libdispatch。

内核方面,主要基于XNU内核Mach部分提供的Mach semaphores和BSD部分提供的kqueue()机制。关于XNU内核的更多细节,请参考即将发行的四月刊《半导体的丰收(下)》。

dispatch/dispatch.h提供了丰富的底层编程接口。

在Cocoa层面,NSOperation被重写,因为使用libdispatch,所以先前使用NSOperation的程序不需改动,就自动享受Grand Central Dispatch的最新特性。

Instruments和GDB提供了非常完整的分析和调试工具。

GCD还有一些工程上的优势。首先,程序的响应速度会更快。GCD让程序员更方便地写多线程程序,因此写一个多线程程序来实现前后台简单多了,极大改善了Mac OS X上应用程序的生态环境。而且GCD的代码块队列开销很小,比传统线程轻量得多。统计表明,传统的Mac OS X上使用的POSIX线程需要数百个计算机汇编指令,占用512KB的内存,而一个代码块队列才用256字节的长度,把块加入队列,只需要15个计算机汇编指令,因此开成百上千个也不费什么事。

其次,线程模式是一种静态的模式,一旦程序被执行,其运行模式就被固定下来了。但用户的计算机配置各不相同,运行时别的程序有可能耗用大量的计算资源。这些都会影响该程序的运行效率。而动态分配系统资源则能很好地解决这个问题。苹果自然也是不遗余力地忽悠开发者使用GCD,因为各个软件共享多核运算的资源,如果GCD被更多的开发者采用,整个苹果平台的生态也就更健康。

而最重要的,还是GCD采用的线程池模式极大简化了多线程编程,也降低了出错的可能性。著名FreeBSD开发者Robert Watson还发布了一个他修改过的Apache,并释出了补丁,声称只需原先1/3至1/2的代码量,就实现了原先的多线程模块,并比原先的效率更好。

如何应用GCD

当然,老王卖瓜,自卖自夸,没有实际的例子,是不能让读者信服的。下面我们就来简单讲解GCD的技术。

首先是块状语法,是一个对C、C++和Objective-C语言的扩展。用来描述一个任务,用^引导的大括号括起来。比如最简单的:

x = ^{ printf(“hello world\n”);}

则 x 就变成了一个块。如果执行:

x();

那么程序会打印hello world出来。当然,blcok像函数一样,可以跟参数,比如:

int spec = 4;

int (^MyBlock)(int) = ^(int aNum){

 return aNum * spec;

}; 

spec = 0;

printf(“Block value is%d”,

MyBlock(4));

这里MyBlock是一个带参数的代码块。

读者看到这里不禁要问,块到底有什么好处?它和C的函数指针有什么不同?我们依然用上面的例子来说明问题,虽然后面我们把spec变量改为0,但事实上在MyBlock创立时,已经生成了一个闭包,因此它最后输出的结果,仍是16,不受spec值改动的影响。这对于搞函数式编程的人来说再熟悉不过了,因此很多开发者亲切地称呼块语法的C扩展为“带lambda的C”。

有了闭包功能的C顿时牛起来—你可以把函数和数据包装在一起—这就是块的真正功能。因为只要一个闭包包含了代码和数据,它的数据就不会被别的闭包轻易改动,所以在它执行时,你根本不用为数据上锁解锁。

有了一系列的代码块后,接下来的事是把代码块扔到队列里。比如最简单的:

dispatch_queue_t queue = dispatch_get_global_queue(0,0);

来创建一个轻量级的队列,然后

dispatch_async(queue,

^{printf(“hello world\n”);});

那这个代码块就被扔进queue这个队列中了。你可以手动依次添加任意多个项目,比如“带着老婆”、“出了城”、“吃着火锅”、“唱着歌”、“突然就被麻匪劫了”等。当然在更多的场合,你会更倾向于使用自动事件源,每当一个事件触发时(比如定时器到点、网络传来包裹,或者用户点击了按钮),相应的代码块被自动添加到队列中。

一旦队列不是空的,GCD就开始分配任务到线程中。拿上面的例子来说,“老婆”、“城”等变量可是封在闭包里的,所以在运行时,不用考虑它们被某个别的闭包改掉(当然也有方法来实现这个功能)。总体而言,这个模式比线程—锁模型简单太多—它的执行是并行的,但思维却是传统的异步思维,对没有学习过系统多线程编程的开发者来说,依然能很容易地掌握。

读者可能要问,如果闭包之间有复杂的依赖关系,需要申明某两个操作必须同步或异步怎么办?比如“出了城”必须在“吃着火锅”之前。在GCD中,可以使用dispatch_async和dispatch_sync来描述这样的依赖关系,而在Cocoa层面,NSOperation中的队列依赖关系甚至可以被描述成有向图。

GCD得到广泛应用

GCD一经推出就得到了广泛的应用。苹果自家的软件Final Cut Pro X、Mail等软件,都采用GCD来实现任务并发和调度,因此Mac OS X 10.6成为了有史以来最快的发行版。从iOS 4开始,iPhone和iPad也加入了GCD的支持。更别提原来使用Cocoa的NSOperation相关接口的程序,无需改动即享受GCD的优惠。

GCD在Mac OS X 10.6发布后,又以libdispatch为名,作为一个独立的开源项目发布。 所需的外围代码,如编译器的块支持、运行库的块支持、内核的支持,也都能在LLVM和XNU等开源项目代码中找到,所以很快被别的操作系统采用。作为Mac OS X的近亲, FreeBSD在一个月后即完整移植了整套GCD技术,并最终在FreeBSD 9.0和8.1中出现。诸多Linux发行版也提供libdispatch的包,使用Linux内核的epoll来模拟FreeBSD的kqueue。2011年5月5日, Windows的移植工作也宣告完成

另外,GCD也成为拯救动态语言的重要法宝。由于受GIL(全局解释锁)的限制,动态语言虽然有操作系统原生线程,但不能在多核处理器上并行执行。而GCD成功绕开了这个限制,如加入GCD支持的Ruby 实现MacRuby就能在多核处理器上高效执行。 因此,在苹果生态圈以外,GCD也会得到越来越多的应用。读者马上还会看到,苹果同时推出的另一项主推技术中也使用了GCD,详细内容请关注四月刊《半导体的丰收(下)》。

 

作者王越,美国宾夕法尼亚大学计算机系研究生,中国著名TeX开发者,非著名OpenFOAM开发者。

本文选自《程序员》杂志2012年03期,未经允许不得转载。如需转载请联系 market@csdn.net

《程序员》2012年杂志订阅送好礼活动火热进行中

 

转播到腾讯微博

----->立刻申请加入《程序员》杂志读者俱乐部,与杂志编辑直接交流,参与选题,优先投稿

8 Responses to “Mac OS X 背后的故事(九)半导体的丰收(中)”

  1. shaka 说道:

    听说宾夕法尼亚大学计算机系要被砍了,是真的吗?

  2. 比分 - live.779a.com|egughb 说道:

    关注博主博客一段时间了,内容很符合我的胃口,以后还会经常来的,…..庚寅年(虎)三月十五 2010-4-28

  3. lispython 说道:

    @shaka 是佛罗里达大学 http://onforb.es/IFAV74

  4. cold89 说道:

    从天朝某词汇的拼音缩写的角度来看,这GCD名字取得还真是不赖

  5. fkusister 说道:

    一个非计算机专业的人写的文章,竟然被发布在CSDN,被一批计算机专业的人所津津乐道,这是为什么?

    这就是微软做的恶,微软已经绑架太多的年轻计算机专业学生和初级程序员。

  6. Yue Wang 说道:

    > 一个非计算机专业的人写的文章,竟然被发布在CSDN,被一批计算机专业的人所津津乐道,这是为什么?

    我是學計算機的好不好。。。

  7. 钢盅郭子 说道:

    “带着老婆”、“出了城”、“吃着火锅”、“唱着歌”、“突然就被麻匪劫了”
    XD

  8. HalfmanG2 说道:

    记得过去某次在某个“说走就走”的公司面试,我写的app用了GCD去处理多线程问题,还被嘲笑了

请评论

preload preload preload
京ICP备06065162