13.9 库和系统调用
计算机语言的演变不断地简化了解决复杂问题的设计和编码工作。随着语言表达能力的发展,我们可以使用它们来解决更复杂的问题。从理论上来讲,一台使用简单语法的图灵机和我们今天所拥有的任何编程(www.cppentry.com)语言一样强大。我们所谓的强大是指可以使用图灵机编写任何算法。只不过那将是一个非常困难的编程(www.cppentry.com)环境,对于汇编语言亦是如此。我们不使用汇编语言开发Web服务器的原因在于其可行性:使用低级语言开发复杂问题的软件解决方案,非常困难并且很费时间。
如果想计算两个整数之和,那么这在诸如C++(www.cppentry.com)之类的语言中非常简单:
- k = i + j;
如果使用汇编语言完成,那么将会有一些微小细节需要我们亲自来处理:
将整数i载入到寄存器X;
将整数j载入到寄存器Y;
将寄存器X和Y中的内容相加;
将相加所得结果存储在整数k的内存地址中。
在高级语言中,所有这些小细节会由编译器处理,我们通常不会去管它。这种简单性使高级语言具备更高的生产力和承担更复杂编程(www.cppentry.com)任务的能力。
隐藏复杂性有很多方法:硬件隐藏和软件隐藏。软件隐藏是通过编译器把我们的源代码转换成汇编指令来实现的。我们也可以亲自实现这种隐藏:将复杂性封装在系统调用、库和类实现中。
以两个字符串的连接操作为例,我们需要分配足够大的内存,使得它能够包含两个字符串和一个结束符:
- {
- char s0;
- char *s1 = "Hello";
- char *s2 = "World";
-
- s0 = (char *) malloc( strlen(s1) + strlen(s2) + 1 );
- strcpy(s0,s1); // 复制第一个字符串
- strcat(s0,s2); // 连接第二个字符串
- ...
- }
在C++(www.cppentry.com)中,所有这些细节都封装在string类的实现中。重载string类的"+"操作符可以将我们的客户代码简化为:
- {
- string s1 = "Hello";
- string s2 = "World";
-
- string s0 = s1 + s2;
- ...
- }
当我们步入简明编程(www.cppentry.com)的更高层次以后,细节工作其实并没有消失,它们会在其他地方完成。从性能角度来讲,我们不能忽略这些幕后工作。这正是性能工程师区别于其他开发者的地方,其他开发者唯一关心的是快速地将所需功能放在一起并迅速发布。而性能开发者必须了解隐藏在代码背后的代价,因为它们可能会对系统性能有深远的影响。
作为具体的例子,接下来我们研究一下pthreads库的实现。该库为用户提供了一个简单的访问UNIX平台线程服务的接口。pthread_mutex_lock()和pthread_mutex_unlock()这两个调用提供互斥锁。如果锁定一个资源,那么就排斥了对该资源的其他访问,直到解锁该资源为止。您无法察觉到的是,在调用pthread_mutex_lock()时,pthreads库的实现会检查您的线程是否已经拥有锁,这样就防止了死锁。当调用pthread_mutex_unlock()时,pthreads库的实现会检查调用者是否真正拥有锁。除非该线程是之前锁定资源的那个线程,否则就不能解锁资源。所有这些必要的出错检查增加了程序执行的路径长度,同时也降低了性能。
在很多情况下,应用程序对锁的使用可能非常简单,那么此时所有这些开销都是浪费的。您的代码设计可能已经确保了这种前提条件,即要进行锁定的线程目前没有拥有锁,要进行解锁的线程是之前锁定资源的线程。在这种情况下,可以建立一种简单的锁定模式,该模式是建立在本机操作系统所提供的原始积木块的基础之上的,这显然更为高效。如果不清楚库的实现细节,那么您可能还不知道自己正在为一些实际上并不需要的功能而付出性能上的代价。
我们继续讨论pthreads库,下面举一个相关的例子。您可以让每个线程都与一个私有数据区域相关联,该区域可能存储了线程的全局变量[NBF96]。如果需要访问线程的私有数据,那么需要一个指向内存中该线程区域的指针。调用pthread_getspecific()可以得到这个指针。pthreads库是通过维护当前线程与私有数据之间的关联来实现这种神奇功能的。由于线程状态可能不断变化,所以这种关联是动态的。必须串行化地访问该集合。因此,pthread_getspecific()的调用隐藏了内部串行化代码。当获取与给定线程关联的数据时,我们必须锁定该集合。如果您关心程序的可伸缩性,这可能就是您需要了解的事情。如果多个线程频繁地调用pthread_getspecific(),那么串行化逻辑将会因为每个线程各自的锁定关系而产生可伸缩性阻塞。
一旦了解了pthread_getspecific()的内部原理,您可能会选择使用其他方式来传递特定线程的数据。比如您可以以函数调用参数的形式向各处传递数据项,而不是将数据项作为线程的全局变量来使用。如果确实需要频繁地访问线程的全局变量,那么您可以只需调用pthread_getspecific()一次,然后将其指针作为函数调用的参数向各处传递。