多线程编程指南

I/O 问题

多线程编程的最大优点之一就是可以提升 I/O 性能。传统的 UNIX API 在这方面给您提供的帮助极少。要么就使用文件系统的功能,要么就整个跳过文件系统。

本节说明如何使用线程通过 I/O 并发性和多缓冲来获得更多灵活性,此外,本节还论述了包含线程的同步 I/O 与包含和不包含线程的异步 I/O 的各种方式之间的差异和相似之处。

I/O 作为远程过程调用

在传统的 UNIX 模型中,I/O 看上去是同步的,就像对 I/O 设备进行远程过程调用一样。调用返回后,I/O 即完成,或者至少看上去已完成。例如,写入请求可能仅导致将数据传输到操作环境中的缓冲区。

此模型的优点在于过程调用的概念是为用户所熟知的。

在传统 UNIX 系统中未使用的一种替代方法是异步模型,在此模型中,I/O 请求仅启动操作。程序必须以某种方式了解操作完成的时间。

异步模型不像同步模型那样简单。但是,异步模型的优点是允许并发 I/O 和在传统单线程 UNIX 进程中的处理。

人为的异步性

通过在多线程程序中使用同步 I/O,可以获得异步 I/O 的大多数优势。使用异步 I/O 时,可以发出请求并随后检查以确定 I/O 完成的时间。可以改用单独的线程同步执行 I/O。随后,主线程或许会在以后的某个时间通过调用 pthread_join(3C) 来检查是否完成了操作。

异步 I/O

在多数情况下,不需要异步 I/O,因为其效果可借助线程得以实现,每个线程执行同步 I/O。但是,在少数情况下,线程不能实现异步 I/O 可以实现的效果。

最直观的示例就是写入磁带机以形成磁带机流。流形式可防止在向磁带机写入内容的同时磁带机停止运行。磁带将高速向前移动,同时提供写入磁带的连续不断的数据流。

要支持流形式,内核中的磁带机应使用线程。内核中的磁带机对中断做出响应时,该磁带机一定会发出排队的写入请求。中断指示以前的磁带写入操作已完成。

线程不能保证会对异步写入进行排序,因为线程执行的顺序是不确定的。例如,您无法指定写入磁带的顺序。

异步 I/O 操作

#include <sys/asynch.h>



int aioread(int fildes, char *bufp, int bufs, off_t offset,

    int whence, aio_result_t *resultp);



int aiowrite(int filedes, const char *bufp, int bufs,

    off_t offset, int whence, aio_result_t *resultp);



aio_result_t *aiowait(const struct timeval *timeout);



int aiocancel(aio_result_t *resultp);

aioread(3AIO)aiowrite(3AIO) 在格式上类似于 pread(2)pwrite(2),但是添加了最后一个参数。对 aioread()aiowrite() 的调用导致启动 I/O 操作或将该操作排入队列。

调用将顺利返回,而不被阻塞,而且调用的状态将在 resultp 所指向的结构中返回。resultpaio_result_t 类型的项,其中包含以下值:

int aio_return;

int aio_errno;

当调用立即失败时,可以在 aio_errno 中找到失败代码。否则,此字段将包含 AIO_INPROGRESS,意味着已成功地将操作排入队列。

等待 I/O 操作完成

通过调用 aiowait(3AIO),可以等待未完成的异步 I/O 操作完成。aiowait() 将返回指向 aio_result_t 结构(随原始 aioread(3AIO) 或原始 aiowrite(3) 调用一同提供)的指针。

此时,aio_result_t 将包含 read(2)write(2) ?òa?|^ao?H?§,前提是要调用其中一个函数而不是异步版本。如果 read()write() 成功,则 aio_return 包含已读取或写入的字节数。如果 read()write() 不成功,则 aio_return 为 -1,且 aio_errno 包含错误代码。

aiowait() 将使用 timeout 参数,该参数指示调用程序将等待的时间。此处的 NULL 指针表示调用程序将无限期等下去。指向包含零值的结构的指针表示调用程序根本不会等待。

您可能会启动异步 I/O 操作,执行某项工作,然后调用 aiowait() 以等待请求完成。或者,可以使用 SIGIO 在操作完成时以异步方式得到通知。

最后,可通过调用 aiocancel() 来取消暂挂的异步 I/O 操作。此例程是使用结果区域地址作为参数来进行调用的。此结果区域标识哪项操作将被取消。

共享的 I/O 和新的 I/O 系统调用

多个线程执行具有相同文件描述符的并发 I/O 操作时,您可能会发现传统的 UNIX I/O 接口不是线程安全的。在 lseek(2) 系统调用设置了文件偏移的位置,会出现不连续 I/O 问题。随后将在接下来的 read(2)write(2) 调用中使用该文件偏移,以指示操作应在文件中的哪个位置开始。当两个或更多线程向同一文件描述符发出 lseeks() 时,将产生冲突。

为避免此冲突,请使用 pread(2)pwrite(2) 系统调用。

#include <sys/types.h>

#include <unistd.h>



ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);



ssize_t pwrite(int filedes, void *buf, size_t nbyte,

    off_t offset);

pread(2) 和 pwrite(2) 的行为方式与 read(2) 和 write(2) 非常类似,但是 pread(2) 和 pwrite(2) 多使用了一个参数(即文件偏移)。可以使用此参数来指定偏移,而无需使用 lseek(2),因此多个线程可以安全地使用这些例程来处理对同一文件描述符的 I/O。

getcputc 的替代项

标准 I/O 也会出现问题。程序员习惯使用例程(如 getc(3C)putc(3C)),这些例程以宏方式实现,且速度非常快。由于 getc(3C)putc(3C) 的速度较快,因此可以在程序的内部循环中使用这些宏,而不必担心效率。

但是,将 getc(3C)putc(3C) 设为线程安全时,宏的使用代价会突然变高。现在,宏至少需要两个内部子例程调用来锁定和解除锁定互斥。

为避开此问题,提供了这些例程的替代版本 getc_unlocked(3C)putc_unlocked(3C)

getc_unlocked(3C)putc_unlocked(3C) 不会获得对互斥的锁定。这些宏的速度像原始非线程安全版本的 getc(3C)putc(3C) 一样快。

但是,要采用线程安全方式使用这些宏,必须使用 flockfile(3C)funlockfile(3C) 显式锁定和释放保护标准 I/O 流的互斥。对其中靠后例程的调用是在循环外进行的。对 getc_unlocked()putc_unlocked() 的调用是在循环内进行的。