【问题标题】:Non-threadsafe file I/O in C/C++C/C++ 中的非线程安全文件 I/O
【发布时间】:2009-07-29 13:26:11
【问题描述】:

在对我们应用程序中的一些性能问题进行故障排除时,我发现 C 的 stdio.h 函数(并且,至少对于我们的供应商而言,C++ 的 fstream 类)是线程安全的。结果,每次我做fgetc这样简单的事情时,RTL都必须获取锁,读取一个字节,然后释放锁。

这对性能不利。

在 C 和 C++ 中获得非线程安全文件 I/O 的最佳方法是什么,以便我可以管理自己的锁定并获得更好的性能?

  • MSVC 提供了_fputc_nolock,GCC 提供了unlocked_stdioflockfile,但是我在我的编译器(CodeGear C++Builder)中找不到任何类似的函数。
  • 我可以使用原始 Windows API,但它不可移植,而且我认为对于一次字符 I/O 而言,它会比解锁的 fgetc 慢。
  • 我可以切换到Apache Portable Runtime 之类的东西,但这可能需要大量工作。

其他人如何处理这个问题?

编辑:由于有些人想知道,我在发布之前已经对此进行了测试。 fgetc 如果可以满足从其缓冲区读取的要求,则不会执行系统调用,但它仍然会执行锁定,因此锁定最终会花费大量时间(为读取单个数据块获取和释放数百个锁从磁盘)。一次不做字符 I/O 将是一个解决方案,但 C++Builder 的 fstream 类不幸使用 fgetc (所以如果我想使用 iostream 类,我会坚持下去),并且我有很多遗留代码使用 fgetc 和朋友从记录样式文件中读取字段(如果不是锁定问题,这将是合理的)。

【问题讨论】:

  • C 的 stdio.h 函数不是线程安全的;那也是你的供应商。
  • 不只是我的供应商;例如,POSIX 需要它。

标签: c++ c multithreading file-io


【解决方案1】:

如果性能明智的话,我根本不会一次执行 IO 一个字符。

【讨论】:

  • 对流的所有操作都会导致 char IO。您必须使用流缓冲区。我已经在下面发布了如何...
【解决方案2】:

fgetc 几乎可以肯定每次调用它时都不会读取一个字节(这里的“读取”是指调用系统调用来执行 I/O)。在其他地方寻找您的性能瓶颈,因为这可能不是问题,并且使用不安全的函数肯定不是解决方案。您执行的任何锁定处理都可能比标准例程执行的处理效率低。

【讨论】:

  • 它不是一次读取一个字节,但每次都可以很好地锁定。顺便说一句,在 POSIX 下锁定是强制性的,有 getc_unlocked() (以及 char 的 _unlocked 变体 char by char IO 函数,以及 lock 函数,以便它们可以受到保护)。
【解决方案3】:

最简单的方法是读取内存中的整个文件,然后为该缓冲区提供您自己的类似 fgetc 的接口。

【讨论】:

    【解决方案4】:

    为什么不只是内存映射文件?内存映射是可移植的(除了在 Windows Vista 中,它需要你跳过希望现在就使用它,笨蛋)。无论如何,将您的文件映射到内存中,并且您是否自己锁定/不锁定生成的内存位置。

    操作系统会处理从磁盘实际读取所需的所有锁定 - 您永远无法消除这种开销。但是另一方面,您的处理开销不会受到除了您自己执行的锁定之外的无关锁定的影响。

    【讨论】:

      【解决方案5】:

      多平台方法非常简单。避免使用标准指定应该使用哨兵的函数或运算符。 sentry 是 iostream 类中的一个内部类,它确保每个输出字符的流一致性,并且在多线程环境中,它为每个正在输出的字符锁定与流相关的互斥锁。这避免了低级别的竞争条件,但仍然使输出不可读,因为来自两个线程的字符串可能会同时输出,如下例所述:

      线程 1 应该写:abc
      线程 2 应该写:def

      输出可能类似于:adebcf 而不是 abcdef 或 defabc。这是因为哨兵被实现为每个角色锁定和解锁。

      该标准为所有处理 istream 或 ostream 的函数和运算符定义了它。避免这种情况的唯一方法是使用流缓冲区和您自己的锁定(例如每个字符串)。

      我编写了一个应用程序,它将一些数据输出到文件并测量速度。如果您在此处添加一个直接使用 fstream 输出而不使用缓冲区和刷新的函数,您将看到速度差异。它使用boost,但我希望这对你来说不是问题。尝试删除所有流缓冲区,看看有无它们的区别。我的情况是性能缺陷是 2-3 倍左右。

      N. Myers 的 following article 将解释 c++ IOStreams 中的语言环境和哨兵是如何工作的。当然,您应该查看 ISO C++ 标准文档,哪些函数使用哨兵。

      祝你好运,
      风向标

      #include <vector>
      #include <fstream>
      #include <iterator>
      #include <algorithm>
      #include <iostream>
      #include <cassert>
      #include <cstdlib>
      
      #include <boost/progress.hpp>
      #include <boost/shared_ptr.hpp>
      
      double do_copy_via_streambuf()
      {
        const size_t len = 1024*2048;
        const size_t factor = 5;
        ::std::vector<char> data(len, 1);
      
        std::vector<char> buffer(len*factor, 0);
      
        ::std::ofstream
          ofs("test.dat", ::std::ios_base::binary|::std::ios_base::out);
        noskipws(ofs);
      
        std::streambuf* rdbuf = ofs.rdbuf()->pubsetbuf(&buffer[0], buffer.size());
      
        ::std::ostreambuf_iterator<char> oi(rdbuf);
      
        boost::progress_timer pt;
      
        for(size_t i=1; i<=250; ++i)
        {
          ::std::copy(data.begin(), data.end(), oi);
          if(0==i%factor)
            rdbuf->pubsync();
        }
      
        ofs.flush();
        double rate = 500 / pt.elapsed();
        std::cout << rate << std::endl;
        return rate;
      }
      
      void count_avarage(const char* op_name, double (*fct)())
      {
          double av_rate=0;
          const size_t repeat = 1;
          std::cout << "doing " << op_name << std::endl;
          for(size_t i=0; i<repeat; ++i)
              av_rate+=fct();
      
          std::cout << "average rate for " << op_name << ": " << av_rate/repeat 
                  << "\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n"
                  << std::endl;
      }
      
      
      int main()
      {
          count_avarage("copy via streambuf iterator", do_copy_via_streambuf);
          return 0;
      }
      

      【讨论】:

        【解决方案6】:

        要考虑的一件事是构建自定义运行时。大多数编译器都提供运行时库的源代码(如果它不在 C++ Builder 包中,我会感到惊讶)。

        这最终可能需要大量工作,但也许他们已经本地化了线程支持以使这样的事情变得简单。例如,对于我正在使用的嵌入式系统编译器,它就是为此而设计的——他们已经记录了添加锁定例程的钩子。然而,这可能是一个令人头疼的维护问题,即使最初证明相对容易。

        另一个类似的途径是与Dinkumware 之类的人讨论使用可提供所需功能的第 3 方运行时。

        【讨论】:

          猜你喜欢
          • 2011-04-05
          • 2011-11-05
          • 2023-03-21
          • 2011-08-07
          • 2023-03-08
          • 2015-05-24
          • 1970-01-01
          • 2014-10-28
          • 1970-01-01
          相关资源
          最近更新 更多