2018-08-14 (星期二)
控制与缓冲机制
标准的I/O链接库实现了三种用户缓冲机制,并且提供了一个接口让开发者可以控制缓存区的类型和大小.不同类型的用户缓冲机制用于不同的目的,适合不懂的情况.下面是三种选项:
未经缓冲(unbuffered)
未执行用户缓冲知己.数据会被直接提交给内核.优于这是执行用户缓冲机制的对立面,所以不常被用到.标准错误在默认情况下下未经缓冲的.
经行缓冲(line-buffered)
所执行的缓冲机制以行为基础.每遇到一个newline字符,缓冲区就会被提交给内核,行缓冲机制适用于输出到屏幕的流.因此,终端机膜人使用此缓冲机制(标准输出在默认情况下是经行缓冲).
经块缓冲(block-buffered)
所执行的缓冲机制以块为基础.本章一开始所探讨的就是这种类型的缓冲机制,适用于文件,默认情况下,与文件相关的所有流都是经块缓冲的.标准I/O会使用full buffering(全缓冲), 这个属于来代替block buffering(块缓冲).
通常,默认的缓冲类型就是正确和理想的缓冲机制.然后标准I/O链接库提供了一个接口,用于控制所使用的缓冲类型:
#include <stdio.h> int setvbuf(FILE *stream, char *buf, int mod, size_t size);
setvbuf()函数会将stream的缓冲类型设定成mode,其值必须是下面其中一个:
_IONFD 未经缓冲
_IOLBF 经行缓冲
_IOFBF 经块缓冲
_IONBF(在此情况下会忽略buf与size)除外,buf会指向一个大小为size字节的缓冲区,而标准I/O将一次作为特定流的缓冲区.如果buf的值为NULL,则glibc会自动分配一个缓冲区.
打开流之后必须调用setvbuf()函数,而且必须对流执行任何其他操作之前进行.
执行成功时,setvbuf()会返回0,否则返回非零
如果提供了缓冲去,关闭流的当时他必须存在.一个常见的错误就是把缓冲区生命成一个自动变量,而在流被关闭之前已经离开自动变量的有效范围,尤其是小心不要把缓冲区设定为main()的局部变量,然后有未被显示关闭流.例如:
#include <stdio.h> int main(void) { char buf[BUFSIZ]; /* 以BUFSIZ 代销的缓冲区讲stdout设成经块缓冲 */ setvbuf(stdout, buf, _IOFBF, BUFSIZ); printf ("ARRR! "); return 0; }
修正此缺陷的方法:在离开有效范围之前显示关闭流,或者将buf设定成一个全局变量.
一般来说,开发者并不担心流上的缓冲机制.标准错误除外,终端机是经行缓冲的,这可以理解,文件是经块缓冲的,这也是可以理解的,块缓冲机制的默认缓冲区大小为BUFSIZ(定义于<stdio.h>),畏怯它通常是一个理想的选择(典型块大小的许多倍)
线程安全
线程就是单一进程中多道执行流程.将其概念化的一个方式就是将他们视为共享地址空间的多个进程.线程随时可以运行,而且可以改写共享的数据,除非数据访问需要注意同步问题或讲它设置成thread-local(线程局部变量,也就是为每个线程准备一个副本).支持线程的操作系统都会提供锁定知己(用于确保相互排斥的程序结构)以确保线程不会彼此猜到对方的脚.标准I/O会使用这些机制.尽管如此,他们通常不够使用.例如,有时你会想锁定一群调用,将临街区(critical region,执行时不会被另一个线程所干扰的程序代码段)从一个I/O操作扩展到多个.在其他情况下,你可能想彻底取消锁定机制提高效率.
标准I/O函数本来就具有线程安全(thread-safe),在内部,他们会关联到一个锁(lock)以及一个锁定计数(lock count),而且每个一打开的流会被关联到一个拥有现成(owning thread).任何线程在发出任何I/O请求之前必须先取得"锁",成为拥有线程.两个或两个以上的线程操作相同的流时不能干扰标准I/O操作,因此单一函数调用的执行环境内,标准I/O操作是不可分割的.
当然实际使用时,许多应用程序所需要的原子性(atomicity)大于个别函数调用的层次.举例来说,如果有多个线程发出请求,虽然个别的写入操作不会互相干扰以及产生混乱的输出,但是应用程序可能会虚妄所有写入请求都能够完成不会中断,为了允许这么做,标准I/O链接库提供了一系列的函数,可用于个别操作和流相关的锁定.
手动文件锁定
函数flockfile()会等到stream不再被锁定后取得流的锁,递增锁定计数,成为流的拥有线程,然后返回:(加锁)
#include <stdio.h>
void flockfile(FILE *stream);
函数funlockfile(FILE *stream)用于递减与stream相关联的锁定计数:(释放锁)
#include <stdio.h>
void funlockfile(FILE *stream);
如果锁定计数变为零,当前线程会放弃流的所有权,另一个线程则可以取得该流的锁.
这些调用可以嵌套.也就是说,单一鲜橙可以多次调用flockfile(),但是直到进程进行相同次数的funlockfile()调用,流才会被解除锁定状态.
ftrylockfile()函数是flockfile()的不受阻挡的版本:
#include <stdio.h>
int ftrylockfile(FILE *stream);
如果stream目前已被锁定,则ftrylockfile()什么事也不会做,而立即返回一个非零值,如果stream目前未被锁定,则他会取得流的锁,递增锁定计数,成为stream的拥有者,然后返回0.
例子:
flockfile (stream); fputs("List of treasure: ", stream); fputs(" (1) 500 gold coins ", stream); fputs(" (2) Wonderfully ornate dishwaro ", stream); funlockfile(stream);
虽然个别的fputs()操作绝不可能造成竞争条件----例如,最后结果不会有"list of treasure" 与其他文字交叉(interleaving)在一起的现象,但是对这个相同的流而言,若他们来自另一个线程的另一个标准I/O操作,则可能出现两个fputs()调用交叉的现象.在理想情况下,一个应用程序的设计不会让多个线程提交I/O给相同的流.然后如果你的应用程序需要这么做,那么你需要范围大于单一函数的不可分割区,再次情况下,flockfile()以及相关联函数可以节省你的时间.