上个月的专栏通过在线程中进行分形计算对上下文切换进行了初步研究。计算的结果更证实了前一个专栏的结论,该专栏显示 Linux 与 Windows 相比大概有 5% 的性能优势。在本月的专栏中,我想单独谈谈上下文切换带来的开销。
现在所有的操作系统都具有上下文切换程序。在 UNIX 中,因为执行上下文切换的子例程是
swtch() 例程,所以称它为
切换程序(switcher)。Linux 和 Windows 都将改变进程上下文的操作称为
调度(scheduling)。在任何情况下,执行调度的效率在多任务操作系统中都是评判可伸缩性好坏的一部分。
在本专栏中,我们将看看操作系统切换上下文的速度能有多快。我们使用的上下文非常简单,它们几乎不执行什么任务,简直很难感觉到它们的存在。要测量一种上下文切换算法的速度,您可以设计一个测试,按指定的顺序执行一定次数的上下文切换操作。测试的结果可以展示上下文切换的过程,也是测量上下文切换操作速度的一种简单的途径。
在 UNIX 中有一种众所周知的简单测试,那就是创建两个进程并在它们之间传送一个令牌,如此往返传送一定的次数。其中一个进程在读取令牌时就会引起阻塞。另一个进程发送令牌后等待其返回时也处于阻塞状态。发送令牌带来的开销与上下文切换带来的开销相比,可以忽略不计。这样,您就可以测算出每秒钟进行的上下文切换的总次数了。
最好的令牌就是将一个字节通过管道来回发送。令牌就是那个字节。在进程“A”将令牌写到管道后,它在读取管道时就会阻塞,直到字节被返回为止。起初在读取管道时会发生阻塞的进程“B”在“A”写字节时会被唤醒,并读取该字节。“B”马上将这个字节写回管道,并等待再次读取该字节。由于字节是通过管道的不同通道传送的,进程不会读到刚写入的字节。这样,进程“A”在通道 1 上写入,在通道 2 上读取。进程“B”在通道 1 上读取,在通道 2 上写入。
这个周而复始的过程会不断继续重复,直到一定次数的令牌传送结束。我编写的 cspipe2a.cpp 展示了这种令牌传送方式。(请参阅本文后面部分的 参考资料来下载 cspipe2a.cpp。)它在 Windows 和 Linux 上都可以编译和运行。这个程序非常简单。
cspipe2a.cpp 将创建第二个线程(主程序是第一个线程),并在两个线程之间来回传送管道令牌。计时的循环如代码片段中所示,
Get() 和
Put() 的定义如下面的片段所示。
清单 1. 计时循环
// //
// PARENT: Writes the first byte. // CHILD:
// //
tstart(); tstart2();
for(i = 0; i < maxcount; i++) { for(i = 0; i < maxcount; i++) {
counter++;
if(!Put(pipeA)) if(!Get(pipeA))
break; break;
if(!Get(pipeB)) if(!Put(pipeB))
break; break;
} }
tend(); tend2();
|
计时例程所作的增强很少,只允许在使用线程的环境中运行两个例程。我将在后面的专栏中向其增加一个输入参数,使这些例程是线程安全的。在这里为了实现目的仅用两个例程只是权宜之计。
下面是
Get() 和
Put() 例程的代码。
清单 2. Get() 和 Put() 例程
int Put(FILED fd)
{
# ifdef _WIN32
if(WriteFile(fd, &counter, sizeof(counter),
&completedA,NULL) == 0) {
printf("WriteFile in Adult Failed after %d writes err=%d\n",
counter, Errno);
return 0;
}
# else
if(write(fd,&counter,sizeof(counter)) != sizeof(counter)) {
printf("Write in A Failed after %d writes err=%d\n",
counter, Errno);
return 0;
}
# endif
return 1;
}
int Get(FILED fd)
{
# ifdef _WIN32
if(ReadFile(fd, &counter, sizeof(counter),
&completedA,NULL) == 0) {
printf("ReadFile in Adult Failed after %d Reads err=%d\n",
counter, Errno);
return 0;
}
# else
if(read(fd,&counter,sizeof(counter)) != sizeof(counter)) {
printf("Read in A Failed after %d Reads err=%d\n",
counter, Errno);
return 0;
}
# endif
return 1;
}
|
我在 Linux(Red Hat 7.2)、Windows 2000 Advanced Server SP2 以及 Windows XP Professional 上运行过 cspipe2a.cpp。假定可以将传送令牌带来的开销忽略不计,那么结果就应该如图 1 所示。
图 1. 使用管道令牌时的上下文切换速度(数字小者为佳)
上图显示了五种运行情况:两种是 Windows 2000 Advanced Server 上的、两种是 Windows XP 上的,还有一种是 Linux 上的。Windows 上的运行情况反映了 My Computer/Properties/Performance 对话框中的改变。可以为应用程序或后台服务对 Windows 2000 Advanced Server 进行优化。Windows XP 能够以 65,536 种方式进行优化。我只试着进行了两种优化。第一种是“Let Windows choose what's best for my computer(让 Windows 选择什么对我的计算机最好)”,第二种是“Adjust for best performance(为了最佳性能而进行优化)”。这个图形反映了 Windows 2000 和 Windows XP Professional 这两种选择,还反映出用这种测量方法,两种设置之间的区别很小。
结果显示出,Windows(2000 和 XP)处理上下文切换都很慢。是这样吗?请回想一下 前面一个关于管道的专栏,我们在该专栏中讨论的结果是 Windows 管道与 Linux 管道相比要慢得多。如果我们选择 Linux 和 Windows 上最好(最快)的功能程序来代表令牌,然后运行同样的测试,结果将会如何呢?
为了找出问题的答案,我编写了一个 cspipe2a.cpp(上面使用过的)的后续版本,名为 csfast5.cpp。(请参阅 参考资料以下载 csfast5.cpp)。该程序在 Windows 上使用临界段(critical section),而在 Linux 上使用 pthread 互斥锁(mutex)。csfast5.cpp 程序的作用域要广一点。它支持任意数目的线程。(在 Linux 上,我们试图使用达到或超过 256 个线程时系统就会崩溃。重新构建内核及库需要使用超过“出厂缺省”的设置。因此,Linux 最终没有超过 128 个线程。)
令牌传送的情况稍有不同,需要加以一定解释。在这里,Windows 临界段和 Pthread 互斥锁都被认为是锁。最初,我们为每个线程创建了一个锁,还创建了一个额外的锁。每个线程开始时都拥有一个“锁”,在 Windows 上是临界段,而在 Linux 上是互斥锁。每个线程锁定自己的锁,然后试图获取邻近线程的锁。当它获得了其它线程的锁时,就会释放当前持有的锁,然后接着试图获取与它当前占有的锁顺序相连的下一个锁。这种按顺序使用锁的方式与通过管道进行上下文切换的情况非常相似。
展示连续锁定和周而复始回到第零个(zeroth)锁的循环的内部循环代码如下一个代码片段所示。
清单 3. 对上下文切换公正计时的内部循环
tstart(&t->_tstart);
for(i = 0; i < maxcount; i++) {
k1 = k + 1;
if(k1 >= ncrits)
k1 = 0;
Lock(k1);
Unlock(k);
if(showme) {
if(showme > 1) {
printf("T%d\n",tnum); fflush(stdout);
}
else if (showme > 2) {
printf("T%d: i=%d %d\n", tnum,i,counterA); fflush(stdout);
}
}
counterA += nthreads;
k = k1;
t->tcounter++;
}
Unlock(k);
tend(&t->_tend);
|
请注意,我修正了 cspipe2a.cpp 中不好的编程方法。计时例程现在可以安全地使用线程。
showme 变量清楚地显示了上下文切换的过程,这样能够验证代码是否按预期运行。当设置了
showme 变量后,性能结果就没什么意义了。
起初所有线程在等待使用邻近线程的锁时都被阻塞。(与最后一个锁相邻的是第零个锁。)被插入到拥挤的锁行列的是最初解开的那个额外的锁。线程要承担的工作是 ― 锁定最初解锁的线程,并对自己的线程解锁,然后试图锁定邻近的线程。(有一段时间我对这些情况感到非常困惑。)
尽管所有线程同步开始工作的情况非常复杂,但程序好象可以运行。此外,额外加入的锁可以不止一个。额外的锁使我们能够看到当调度程序允许多个线程同时运行时会发生什么情况。程序包含了一个调试输出,用来验证操作的正确性。
图 2 显示了只有一个额外锁的情况,它应该类似于管道的情况。图中包括使用管道时两个线程的情况中的几点。您可以看到,管道令牌比 Windows 上的临界段开销要大得多。
图 2. 单令牌的上下文切换时间(数字大者较差)
图 2 显示了第一组点,它们周围的框为 cspipe2a.cpp 程序决定的上下文切换测量方法。剩下的点是由 csfast5.cpp 程序生成的。上图表明,当为令牌选择了一个合理的原语时,Windows 在上下文切换速度方面要好过 Linux。我们再来看看 进程同步专栏中与原语有关的一些时间。
| 接口 | 操作系统(每次调用所用的微秒数) | ||
| Win2k AS | WinXP | Linux 2.4.2 | |
| Mutex | 2.629 | 2.191 | |
| Sema | 2.629 | 2.149 | |
| Critical Section | 0.046 | 0.129 | |
| SVR5 Semaphore | 1.828 | ||
| POSIX Semaphore | 0.487 | ||
| pthread_mutex | 0.262 | ||
我们可以从图 2 和表 1 观察到,Windows 2000 的上下文切换开销大概为:
1.85 usec - .046 usec = 1.8 usec
Windows XP 缩短了它的上下文切换时间,但增加了临界段的时间,每次上下文切换大约为:
1.61 usec - .129 usec = 1.48 usec
。从 Windows 2000 到 Windows XP 的时间缩减量是一个很大的改进。Linux 上下文切换时间可以粗略估计为:
3.56 usec - .262 usec = 3.2 usec
上面的测量表明,要选择并运行一个不同的上下文,Linux 大概比 Windows 多花一倍的时间。这个结论适用的情况是:每个线程几乎不做任何工作,只是得到 CPU 控制权后马上放弃。
这种特殊的测试不能代表很多(如果有)实际的情况。它的确清楚地显示了每种操作系统的上下文切换速度可能具有的一些局限性。我的下个专栏将加入反映实际情况的工作,用分形 CPU 周期消耗程序来测量一个或多个线程在给定时间中能够完成多少次分形计算。毕竟,如果不完成一些实际工作,上下文切换没有任何意义。
我编写并提供了两个程序,用来确定上下文切换带来的开销。程序在 Windows 2000 Advanced Server Service Pack 2、Windows XP Professional 以及 Linux 2.4.2 Red Hat 7.2 下编译并运行。这两个程序清楚地展示了在测量上下文切换时间时选用了上下文令牌后的效果。在这里所示的情况下,单字节管道令牌在 Windows 上要占用很长的上下文切换时间 ― 比 Linux 要慢很多。在选用了最佳的 Windows 和 Linux 功能程序后,结果就变了。Windows(Windows 2000 和 Windows XP)在上下文切换速度上的性能明显优于 Linux。
您可以下载 cspipe2a.cpp 和 csfast5.cpp(请参阅 参考资料)。我鼓励您复制它们并在您自己的硬件上运行。如果您希望比较两个操作系统,所有工作都应该在使用相同硬件的前提下进行。我已经在看上去相同的两台 ThinkPad 600X 机器上看到了不同的结果。比较使用不同硬件的结果会使结果没有保证。
- 请单击文章顶部或底部的
讨论,参加关于本文的
讨论论坛。
- 下载本专栏中涉及的应用程序的源代码:
- 阅读 Ed 的前几篇有关“运行时”的专栏文章:
- 介绍性专栏( developerWorks,2001 年 4 月)
- Block memory copy( developerWorks,2001 年 6 月)
- Block memory copy, Part 2( developerWorks,2001 年 7 月)
- Pipes in Linux,Windows 2000,and Windows XP( developerWorks,2001 年 10 月)
- Synchronizing processes and threads( developerWorks,2001 年 10 月)
- Programming sockets( developerWorks,2001 年 11 月)
- Managing processes and threads( developerWorks,2002 年 2 月)
- Scheduling threads( developerWorks,2002 年 6 月)
-
Next Generation POSIX Threading项目旨在解决与 Linux pthread 库相关的许多问题,包括性能问题。IBM 的
Linux Technology Center的几位成员是其中的核心贡献者。
- 请在
developerWorksLinux 专区查找更多
Linux 文章。

Ed 管理着 IBM Software 小组的 Microsoft Premier Support,并为 Linux 和 Windows 软件开发人者撰写每周一期的时事通讯。您可以通过 egb@us.ibm.com与 Ed 联系。