第70页 | 现代操作系统 | 阅读 ‧ 电子书库

同步阅读进度,多语言翻译,过滤屏幕蓝光,评论分享,更多完整功能,更好读书体验,试试 阅读 ‧ 电子书库

2.2.9 使单线程代码多线程化

许多已有的程序是为单线程进程编写的。把这些程序改写成多线程需要比直接写多线程程序更高的技巧。下面我们考察一些其中易犯的错误。

先考察代码,一个线程的代码就像进程一样,通常包含多个过程,会有局部变量、全局变量和过程参数。局部变量和参数不会引起任何问题,但是有一个问题是,对线程而言是全局变量,并不是对整个程序也是全局的。有许多变量之所以是全局的,是因为线程中的许多过程都使用它们(如同它们也可能使用任何全局变量一样),但是其他线程在逻辑上和这些变量无关。

作为一个例子,考虑由UNIX维护的errno变量。当进程(或线程)进行系统调用失败时,错误码会放入errno。在图2-19中,线程1执行系统调用access以确定是否允许它访问某个特定文件。操作系统把返回值放到全局变量errno里。当控制权返回到线程1之后,并在线程1读取errno之前,调度程序确认线程1此刻已用完CPU时间,并决定切换到线程2。线程2执行一个open调用,结果失败,导致重写errno,于是给线程1的返回值会永远丢失。随后在线程1执行时,它将读取错误的返回值并导致错误操作。

图 2-19 线程使用全局变量所引起的错误

对于这个问题有各种解决方案。一种解决方案是全面禁止全局变量。不过这个想法不一定合适,因为它同许多已有的软件冲突。另一种解决方案是为每个线程赋予其私有的全局变量,如图2-20所示。在这个方案中,每个线程有自己的errno以及其他全局变量的私有副本,这样就避免了冲突。在效果上,这个方案创建了新的作用域层,这些变量对一个线程中所有过程都是可见的。而在原先的作用域层里,变量只对一个过程可见,并在程序中处处可见。

图 2-20 线程可拥有私有的全局变量

访问私有的全局变量需要有些技巧,不过,多数程序设计语言具有表示局部变量和全局变量的方式,而没有中间的形式。有可能为全局变量分配一块内存,并将它转送给线程中的每个过程作为额外的参数。尽管这不是一个漂亮的方案,但却是一个可用的方案。

还有另一种方案,可以引入新的库过程,以便创建、设置和读取这些线程范围的全局变量。首先一个调用也许是这样的:

create_global("bufptr");

该调用在堆上或在专门为调用线程所保留的特殊存储区上替一个名为bufptr的指针分配存储空间。无论该存储空间分配在何处,只有调用线程才可访问其全局变量。如果另一个线程创建了同名的全局变量,由于它在不同的存储单元上,所以不会与已有的那个变量产生冲突。

访问全局变量需要两个调用:一个用于写入全局变量,另一个用于读取全局变量。对于写入,类似有

set_global("bufptr",&buf);

它把指针的值保存在先前通过调用create_global创建的存储单元中。如果要读出一个全局变量,调用的形式类似于

bufptr=read_global("bufptr");

这个调用返回一个存储在全局变量中的地址,这样就可以访问其中的数据了。

试图将单一线程程序转为多线程程序的另一个问题是,有许多库过程并不是可重入的。也就是说,它们不是被设计成下列工作方式的:对于任何给定的过程,当前面的调用尚没有结束之前,可以进行第二次调用。例如,可以将通过网络发送消息恰当地设计为,在库内部的一个固定缓冲区中进行消息组合,然后陷入内核将其发送。但是,如果一个线程在缓冲区中编好了消息,然后被时钟中断强迫切换到第二个线程,而第二个线程立即用它自己的消息重写了该缓冲区,那会怎样呢?

类似地还有内存分配过程,例如UNIX中的malloc,它维护着内存使用情况的关键表格,如可用内存块链表。在malloc忙于更新表格时,有可能暂时处于一种不一致的状态,指针的指向不定。如果在表格处于一种不一致的状态时发生了线程切换,并且从一个不同的线程中来了一个新的调用,就可能会由于使用了一个无效指针从而导致程序崩溃。要有效的解决所有这些问题意味着重写整个库。做这件事并非是无效的行为。

另一种解决方案是,为每个过程提供一个包装器,该包装器设置一个二进制位从而标志某个库处于使用中。在先前的调用还没有完成之前,任何试图使用该库的其他线程都会被阻塞。尽管这个方式可以工作,但是它会极大地降低系统潜在的并行性。

接着考虑信号。有些信号逻辑上是线程专用的,但是另一些却不是。例如,如果某个线程调用alarm,信号送往进行该调用的线程是有意义的。但是,当线程完全在用户空间实现时,内核根本不知道有线程存在,因此很难将信号发送给正确的线程。如果一个进程一次仅有一个警报信号等待处理,而其中的多个线程又独立地调用alarm,那么情况就更加复杂了。

有些信号,如键盘中断,则不是线程专用的。谁应该捕捉它们?一个指定的线程?所有的线程?还是新创建的弹出式线程?进而,如果某个线程修改了信号处理程序,而没有通知其他线程,会出现什么情况?如果某个线程想捕捉一个特定的信号(比如,用户击键CTRL+C),而另一个线程却想用这个信号终止进程,又会发生什么情况?如果有一个或多个线程运行标准的库过程以及其他用户编写的过程,那么情况还会更复杂。很显然,这些想法是不兼容的。一般而言,在单线程的环境中信号已经是很难管理的了,到了多线程环境中并不会使这一情况变得容易处理。

由多线程引入的最后一个问题是堆栈的管理。在很多系统中,当一个进程的堆栈溢出时,内核只是自动为该进程提供更多的堆栈。当一个进程有多个线程时,就必须有多个堆栈。如果内核不了解所有的堆栈,就不能使它们自动增长,直到造成堆栈出错。事实上,内核有可能还没有意识到内存错是和某个线程栈的增长有关系的。

这些问题当然不是不可克服的,但是却说明了给已有的系统引入线程而不进行实质性的重新设计系统是根本不行的。至少可能需要重新定义系统调用的语义,并且不得不重写库。而且所有这些工作必须与在一个进程中有一个线程的原有程序向后兼容。有关线程的其他信息,可以参阅(Hauser等人,1993;Marsh等人,1991)。

请支持我们,让我们可以支付服务器费用。
使用微信支付打赏


上一页 · 目录下一页


下载 · 书页 · 阅读 ‧ 电子书库