为什么通过创建新线程读取文件比不使用新线程花费更多时间?

Why does reading a file by creating a new thread take more time than not using a new thread?

提问人:Cardinal 提问时间:6/10/2023 最后编辑:chqrlieCardinal 更新时间:6/10/2023 访问量:66

问:

所以我正在阅读一段时间,长度为 3.5 GB(实际上这是文件大小的一半。我正在阅读文件的一半)。我最初的想法是将 7GB 分成两半,并在两个单独的线程中读取一半,看看我是否可以在没有任何线程的情况下一次性读取整个文件来提升性能。

但是,仅仅在新创建的线程中读取一半文件比在没有任何线程的情况下读取整个文件花费的时间要多得多。为什么会有这样的差异?

这是没有任何线程的代码:-

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s <filepath>\n", argv[0]);
        exit(1);
    }
    struct stat info;
    if (stat(argv[1], &info) < 0) {
        perror("stat()");
        exit(1);
    }
    long size = info.st_size;

    long count = 0;
    FILE *fptr = fopen(argv[1], "r");
    if (fptr == NULL) {
        perror("fopen()");
        exit(1);
    }
    int ch = 0;
    while (count != size / 2) {
        ch = fgetc(fptr);
        count++;
    }
    printf("read bytes: %ld\n", count);
}

以上代码平均需要 8-10 毫秒才能完成。

现在,使用 pthreads 正在做同样的事情,

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

FILE *fptr1;
long size = 0;
long by = 0;

void *read_file(void *param) {
    long count = 0;
    int ch = 0;
    while (count != size / 2) {
        ch = fgetc(fptr1);
        count++;
    }
    by = count;
    return NULL;
}

int main(int argc, char **argv) {
    if (argc < 2) {
        exit(1);
    }

    struct stat info;
    if (stat(argv[1], &info) < 0) {
        perror("stat()");
        exit(1);
    }

    size = info.st_size;

    pthread_t thread1;
    fptr1 = fopen(argv[1], "r");

    if (pthread_create(&thread1, NULL, &read_file, NULL) < 0) {
        perror("thread()");
        exit(1);
    }

    pthread_join(thread1, NULL);

    printf("bytes: %ld\n", by);
}

此代码执行完全相同的操作,平均需要 65-70 秒。

为什么与非螺纹版本相比,螺纹外壳需要这么多时间?用两个线程将文件分成两半有什么意义吗?

另外,我知道会是一个更好的选择,完全同意。我故意使用,因为我不想设置缓冲区等等。由于在两个版本中都使用了,我想答案在于线程和 .fread()fgetcfgetc()fgetc()

谢谢你的帮助。

C 多线程 文件 io pthreads

评论

2赞 Gerhardh 6/10/2023
与您的实际问题无关,但“在两个单独的线程中读取一半”听起来好像您希望将纯读取拆分为线程可能会提高速度。无论有多少线程等待相同的数据,读取文件都不会更快。只有当您对数据进行复杂的计算时,其中 CPU 功率是相关的,您才可能会加快速度。否则,更多的线程只会增加开销。您的文件系统不会通过添加线程而变得更快。
0赞 Andrew Henle 6/10/2023
@Gerhardh 有些(非自由)文件系统确实变得更快,因为它们要复杂得多,并且可以分布在多个磁盘/设备上。例如: en.wikipedia.org/wiki/GPFS 您也许可以创建一个可以做到这一点的 ZFS 文件系统,但 ZFS 在性能方面是一头猪,所以如果您需要速度,您首先不会使用它。完成所有操作后,您的应用程序/代码需要以特定方式访问文件系统以利用这些功能。但对于最常见的简单单磁盘文件系统,多线程会减慢速度。
0赞 Andrew Henle 6/10/2023
如果存储是旋转磁盘,速度会更糟。由于文件系统尝试使文件在磁盘上保持连续,因此仅使用一个线程读取会减少磁盘查找 - 磁盘头可以停留在包含文件大块的磁盘轨道上。多个读取线程导致磁盘头必须查找文件的不同块,这需要大量时间。从旋转磁盘进行真正的随机小块读取可以轻松地将读取吞吐量降低到低 KB/秒范围 - 消费级 SATA 驱动器每秒 50 次 IO 操作乘以 512 字节......
0赞 n. m. could be an AI 6/10/2023
线程 cide 具有 UB,因为它在不同步的情况下修改全局变量。
0赞 n. m. could be an AI 6/10/2023
“因为我不想设置缓冲区之类的。”因此,请承担后果(缓慢)。公平交易?

答:

1赞 chqrlie 6/10/2023 #1

以下是线程代码速度较慢的多种原因:

  • 不建议在多线程应用程序中使用标准流,并且有明显的缺点:针对不需要锁定结构的非线程程序进行了优化。在多线程应用程序中,为了允许对结构进行一致的并发访问,所有标准流函数都必须使用锁来序列化对流结构的访问,即使它在单个线程中使用也是如此,因为库无法断言这一点。与从缓冲区读取单个字节所涉及的最小任务相比,这非常慢。fgetc()FILEFILE

    如果流在单个线程中使用,则可以使用 来绕过此开销,但对于更复杂的流函数(如 )没有等效项。 如果一次读取大块,应该不是问题。fgetc_unlocked()fgetc()fgetsfread

  • 此外,线程版本似乎实现了完全相同的任务,但它对 和 使用了全局变量,这对生成的代码有影响。此外,该函数需要在每次迭代时从内存中重新加载值,因为编译器不能假定这不会更改它们。这可能会阻止此版本中的进一步优化。sizefptr1fgetc()

    请注意,通过全局变量将值传递给线程是糟糕的设计,您应该使用分配结构并传递指针作为参数。param

  • 请注意,如果为每个线程创建多个线程来处理文件的单独部分,则会遇到另一个问题:线程将竞争从存储设备上不同位置的文件中读取块,这在某些设备(如硬盘)上可能效率非常低,因为这些访问将导致磁头移动,每次访问的延迟通常为 10 毫秒。按顺序读取文件可能会效率提高一个数量级。文件系统布局和缓存、特定设备特征和其他因素可能会影响性能并使其不可重现。

为了进行快速测试,请尝试将函数更改为:read_file

void *read_file(void *param) {
    long local_size = size;
    FILE *local_fptr = fptr1;
    long count = 0;
    int ch;
    while (count != local_size / 2) {
        ch = fgetc_unlocked(local_fptr);
        count++;
    }
    by = count;
    return NULL;
}

评论

1赞 n. m. could be an AI 6/10/2023
真正的解释是线程代码中的 fgetc 等于自杀。
0赞 Cardinal 6/10/2023
@n.M. 是的,所说的才是真正的问题。当我使用 fgetc_unlock() 时,时间与非线程版本相同。
0赞 chqrlie 6/10/2023
@n.M:确实是流锁的问题所在。我用解释更新了答案。