C / C ++中的非线程安全文件I / O.

时间:2009-07-29 13:26:11

标签: c++ c multithreading file-io

在对我们的应用程序中的一些性能问题进行故障排除时,我发现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能够满足从其缓冲区读取的内容,那么fstream不会进行系统调用,因此锁定最终会占用大量的时间(数百个锁定用于获取和释放单个块从磁盘读取的数据)。不做一次一个字符I / O将是一个解决方案,但不幸的是C ++ Builder的iostream类使用fgetc(所以如果我想使用fgetc类,我会坚持使用它有很多遗留代码使用{{1}}和朋友来读取记录样式文件中的字段(如果不是锁定问题,那将是合理的。)

6 个答案:

答案 0 :(得分:3)

如果这是明智的表现,我根本不会一次做IO一个字符。

答案 1 :(得分:1)

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

答案 2 :(得分:1)

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

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

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

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

输出可能如下所示:adebcf而不是abcdef或defabc。这是因为实现了哨兵来锁定和解锁每个角色。

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

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

N. Myers的following article将解释c ++ IOStream中的语言环境和哨句如何工作。当然,你应该查阅ISO C ++标准文档,其功能使用哨兵。

祝你好运,
Ovanes

#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;
}

答案 5 :(得分:1)

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

这可能最终会成为很多工作,但也许他们已经将线程支持本地化,以使这样的事情变得容易。例如,使用我正在使用的嵌入式系统编译器,它就是为此设计的 - 它们有记录的钩子来添加锁定例程。然而,这可能是一个维护问题,即使最初证明它相对容易。

另一个类似的路线是与Dinkumware之类的人讨论如何使用提供所需功能的第三方运行时。