me

I/O Stream


为什么要写这篇文章?

原因是在为 grammy 提 PR 的时候,Deno 1.42.0 在使用 using 去释放文件描述符时有 bug(1.42.4 修复了):

async function getStream() {
  using fd = await Deno.open("README.md")
  return fd.readable
}

文件描述符被释放了,这时候返回是不可能有资源的,返回的 stream 也是没有写到缓冲区的,所以这段代码本身就是有问题的,然而直接返回 stream,并没有关闭文件描述符会造成资源泄漏(🤡)。

当然在 Deno 中完全可以使用 const 然后返回一个异步迭代器的方式,异步迭代器在 done: true 的时候会主动释放文件描述符,具体参考:https://github.com/denoland/deno/issues/23481

I/O 流的分类

缓冲

提供缓冲(Buffer)是为了提高内存和 I/O 设备之间的交换速度设计的,例如每次需要对硬盘进行 100 次读写,而有缓冲区之后可能就是每秒 10 次;缓存是 CPU 和内存之间的数据交换,弥补内存的频率过低问题

Rust 中使用 I/O 缓冲和不使用缓冲的示例:https://nnethercote.github.io/perf-book/io.html#buffering。Rust 中缓冲区的默认大小是 0x2000

fn main() -> Result<(), Box<dyn Error>> {
    let mut writer = BufWriter::new(File::create("README.md")?);
    let content = b"# Hello, world!";
    for c in content.iter() {
        write!(writer, "{}", *c as char)?;
    }

    Ok(())
}

Go 中使用 bufio 中的 NewWriterNewReader 来缓冲 io.Writerio.Reader,它的默认大小是 4096 个字节

func main() {
        writer := bufio.NewWriter(os.Stdout)
        fd, err := os.Open("go.mod")
        defer fd.Close()
        if err != nil {
                fmt.Printf("打开文件失败")
        }
        writer.ReadFrom(fd)
        writer.Flush()
}

Go 中的 I/O 流甚至更简单,只要实现 Writer interface 就是可读流,实现 Reader interface 就是可写流。

bytes 库提供了一个既可读也可写的 Buffer 类型

func main() {
        producer := []string{
                "Channels orchestrate mutexes serialize",
                "Cgo is not go",
                "Errors are values",
                "Don't panic",
        }

        var buffer bytes.Buffer
        for _, p := range producer {
                n, err := buffer.Write([]byte(p))
                if err != nil {
                        fmt.Println(err)
                        os.Exit(1)
                }

                if n != len(p) {
                         fmt.Println("failure")
                         os.Exit(1)
                }
        }

        fs, err := os.Create("data.txt")
        if err != nil {
                fmt.Println(err)
                os.Exit(1)
        }

        buffer.WriteTo(fs)
}

C 中提供了低级 I/O标准 I/O(fopen、fwrite 等带有 I/O)以及高级 I/O

int main(){
    for (int i = 0; i < 10000; i++)
        fputs("hello world", stdout);
    return 0;
}

我们可以使用以下命令来查看缓冲之后会执行多少次 write(Linux),如果没有缓冲,将是执行 10000 次。

strace ./main 2>err
grep "write(" err | wc -l

在 C 中很容易就可以写一个带缓冲的 I/O 流

#include <stdio.h>

int main() {
    // 文件指针
    FILE *fp;
    // 用于存储读取的字符
    char ch;

    // 打开文件用于读取
    fp = fopen("main.go", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 读取文件内容并打印
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // 关闭文件
    fclose(fp);
    return 0;
}

关于 JavaScript 中的 ReadableStreamWriteableStream 也是同样的,它们本身就是可缓冲的。