U
U
User7002021-10-21 21:42:11
C++ / C#
User700, 2021-10-21 21:42:11

Why does WinAPI do a synchronous write?

Encryptor program (but relevant for any file handler). Before the cycle, the following is performed: the beginning of reading a new portion; creation of independent data (for a new portion); waiting for the end of reading (new portion). In the cycle, the following is performed: the beginning of reading a new portion; processing of the current portion (using independent data for the current portion); then start recording the current processed portion; creation of independent data (for a new portion); waiting for the end of reading (new portion) and the end of writing (current portion). After the loop: processing the current chunk (using independent data for the current chunk); then start recording the current processed portion; waiting for the end of the recording (the current portion).
I organize asynchronous access to the file using WinAPI with the following class:

#include <exception>
#include <windows.h>
#include <cstdint>

class AsyncBinFile
{
public:
  class Err : public std::exception {};
  class ErrOpen : public Err {
    public: const char* what() const noexcept override {
      return "cannot open file";
    } };
  class ErrWriteRead : public Err {
    public: const char* what() const noexcept override {
      return "file write / read error";
    } };
  AsyncBinFile() = delete;
  enum mode_t {WriteNew, Write, Read};
  enum buf_mode_t {NoBuffering = true, Buffering = false};
  AsyncBinFile (const char* name, mode_t mode, buf_mode_t buf_mode = Buffering);
  void close();
  ~AsyncBinFile() {close();}
  AsyncBinFile(const AsyncBinFile&) = delete;
  AsyncBinFile& operator=(const AsyncBinFile&) = delete;
  HANDLE file_handler() const noexcept {return fh;}
  void begin_write (const uint8_t* a, uint32_t n);
  void begin_read (uint8_t* a, uint32_t n);
  void wait();
  void seek (uint64_t offset)
    {ovl.OffsetHigh = DWORD(offset>>(sizeof(DWORD)*8)); ovl.Offset = DWORD(offset);}
  uint64_t tell()
    {return (uint64_t(ovl.OffsetHigh)<<(sizeof(DWORD)*8)) | ovl.Offset;}
  uint64_t length();
private:
  OVERLAPPED ovl;
  HANDLE fh;
  DWORD _n;
};


AsyncBinFile::AsyncBinFile (const char* name, mode_t mode, buf_mode_t buf_mode)
{
  ovl.Internal = 0; ovl.InternalHigh = 0;
  fh = CreateFile (
    name,
    (mode == WriteNew || mode == Write) ? GENERIC_WRITE : GENERIC_READ,
    /*0*/ FILE_SHARE_READ | FILE_SHARE_WRITE, 0,
    (mode == WriteNew) ? CREATE_NEW
      : ((mode == Read) ? OPEN_EXISTING : OPEN_ALWAYS),
    /*FILE_ATTRIBUTE_NORMAL*/0 | FILE_FLAG_OVERLAPPED
      | (buf_mode ? (FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH) : 0),
    0
  );
  if (fh == INVALID_HANDLE_VALUE) throw ErrOpen();
  ovl.hEvent = CreateEvent (0, /*TRUE*/FALSE, FALSE, 0);
  ovl.OffsetHigh = 0; ovl.Offset = 0; _n = 0;
  if (ovl.hEvent == INVALID_HANDLE_VALUE) throw ErrOpen();
}

void AsyncBinFile::close()
{
  if (fh != INVALID_HANDLE_VALUE) {
    CancelIo (fh);
    if (ovl.hEvent != INVALID_HANDLE_VALUE) CloseHandle (ovl.hEvent);
    CloseHandle (fh);
    fh = INVALID_HANDLE_VALUE;
    if (_n != 0) throw ErrWriteRead();
  }
}

void AsyncBinFile::begin_write (const uint8_t* a, uint32_t n)
{
  if (_n != 0) throw ErrWriteRead();
  _n = n;
  BOOL r = WriteFile (fh, a, n, /*&act_n*/nullptr, &ovl);
  if (!r && GetLastError() != ERROR_IO_PENDING) throw ErrWriteRead();

  std::cout << "O="<<r<<'\n';
}

void AsyncBinFile::begin_read (uint8_t* a, uint32_t n)
{
  if (_n != 0) throw ErrWriteRead();
  _n = n;
  BOOL r = ReadFile (fh, a, n, /*&act_n*/nullptr, &ovl);
  if (!r && GetLastError() != ERROR_IO_PENDING) throw ErrWriteRead();

  std::cout << "I="<<r<<'\n';
}

void AsyncBinFile::wait()
{
  //while (!HasOverlappedIoCompleted(&ovl));
  DWORD act_n;
  BOOL r = GetOverlappedResult (fh, &ovl, &act_n, TRUE);
  if (r == 0 || act_n != _n) throw ErrWriteRead();
  seek (tell() + _n); _n = 0;
}

uint64_t AsyncBinFile::length()
{
  DWORD high;
  DWORD low = GetFileSize (fh, &high);
  return (uint64_t(high)<<(sizeof(DWORD)*8)) | low;
}

Here you can see fragments of the debug output and some alternative options for the parameters, replacing which, trying to achieve operability. In particular, began to specify FILE_SHARE_READ | FILE_SHARE_WRITE in attr. access, and removed FILE_ATTRIBUTE_NORMAL, also changed the FALSE / TRUE setting of the event auto-reset mode (ovl.hEvent).
The problem is that when measuring the operating time, the actual synchronism of the write operation was found in all records except the first one. Those. according to the idea, begin_write is fast, and waiting for the end of wait is long, unless, of course, little was done after begin_write (for example, they commented out the corresponding lines). It displays "I=1" if there was a read from the cache (otherwise "I=0"). "O=0" apparently should always be displayed.
Perhaps an error at the beginning of writing another file (record request) before waiting for the end of reading another. But of course I counted on the presence of a system queue. Moreover, requests go to different entities, different files; and the algorithm does not depend on the order in which operations are performed. at the end of the cycle, we wait for the end of both reading and writing.
However, I changed the code for testing, so now in a loop: start reading a new portion; processing of the current portion (using independent data for the current portion); waiting for the end of reading (new portion); start recording the current processed portion; creation of independent data (for a new portion); waiting for the end of the recording (the current portion).
Here is the main code:
static uint64_t buf_a [(BUF_LEN+7)/8];
static uint64_t buf_b [(BUF_LEN+7)/8];
static uint64_t buf_g [(BUF_LEN+7)/8];

{ /*...*/
  AsyncBinFile fin (_f_i, AsyncBinFile::Read);
  AsyncBinFile fout (_f_o, AsyncBinFile::WriteNew);

  uint64_t bytes_to_proc, file_len;
  uint32_t portion, portion_next;
  uint64_t *buf, *buf_next;

  file_len = fin.length();
  bytes_to_proc = file_len;
  buf = buf_b; buf_next = buf_a;

  init = Encrypt64 (init, key);
  portion_next = (bytes_to_proc > BUF_LEN) ? BUF_LEN : bytes_to_proc;
auto x = chrono::steady_clock::now();
  fin.begin_read ((uint8_t*)(buf_next), portion_next);
cout << 'i' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
  //init = MakeGamma (buf_g, portion_next, init, key);
x = chrono::steady_clock::now();
  fin.wait();
cout << 'w' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
  portion = portion_next;
  bytes_to_proc -= portion;
  portion_next = (bytes_to_proc > BUF_LEN) ? BUF_LEN : bytes_to_proc;
  while (portion_next > 0)
  {
    swap (buf, buf_next);
x = chrono::steady_clock::now();
    fin.begin_read ((uint8_t*)(buf_next), portion_next);
cout << 'i' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
    //XOR (buf, buf_g, portion);
x = chrono::steady_clock::now(); //
    fin.wait(); //  перенос из низа
cout << 'w' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n'; //

x = chrono::steady_clock::now();
    fout.begin_write ((uint8_t*)(buf), portion);
cout << 'o' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
    //init = MakeGamma (buf_g, portion_next, init, key);
//x = chrono::steady_clock::now();
//		fin.wait();       перенос вверх
//cout << 'w' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
x = chrono::steady_clock::now();
    fout.wait();
cout << 'w' << chrono::duration<double>(chrono::steady_clock::now()-x).count() << '\n';
    //PB.show (file_len-bytes_to_proc, file_len);
    portion = portion_next;
    bytes_to_proc -= portion;
    portion_next = (bytes_to_proc > BUF_LEN) ? BUF_LEN : bytes_to_proc;
  }
  XOR (buf_next, buf_g, portion);
  fout.begin_write ((uint8_t*)(buf_next), portion);
  fout.wait();
/*...*/ }

The execution times (in sec.) of the operations "read start", "start write", "wait for the end of the last operation" should be displayed with the prefixes "i", "o", "w" respectively. (except for the very last entry after the loop, but that doesn't matter).
When processing a file of about 100 MB, I got this output:
I=0
i0.234375
w1.26562
I=0
i0.03125
w1.48438
O=0
o0
w0.125
I=1
i0
w0
O=0
o0.0625
w0
 . . .

When we see "I=0 \ i0.015625 \ w0.296875", this means that there was no read from the cache and the read initialization operation took 16 ms, and continued for another 299 ms. If we see "I=0 \ i0 \ w0", then the data was loaded from the cache. But only at the first entry "O=0 \ o0 \ w0.125" is displayed, and then vice versa "O=0 \ o0.234375 \ w0" and similarly. Those. the entire recording goes at the moment of initialization of the beginning of the recording.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
A
Armenian Radio, 2021-10-21
@gbg

In a manual it is directly written that the asynchronous operation can be executed and synchronously - here as venda decides.
By the way, why did Boost::asio not please you?

U
User700, 2021-10-22
@User700

Perhaps this is the case https://docs.microsoft.com/en-RU/troubleshoot/wind...


File extension
Another reason for synchronous completion of I/O operations is the operations themselves. On Windows, any write operation to a file that extends its length will be synchronous.
Note
Applications can make the previously mentioned write operation asynchronous by using a function to change the valid file data length and then issuing SetFileValidData WriteFile .

It is necessary to try
But it still remains unclear what is the problem with not writing the last portion in the FILE_FLAG_NO_BUFFERING mode, even taking into account the alignment of everything and everything on 512; possibly in a CancelIO call in close (although there is a wait before that).

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question