I'm currently working on a project that requires a multi-threaded approach. This project is currently platform specific, i.e. Windows.
This is how my read code looks like:
#pragma once
#include <Windows.h>
#include <cstdint>
#include <expected>
#include <string>
#include "Utils/error.hpp"
class mt_reader {
protected:
HANDLE _handle_disk = INVALID_HANDLE_VALUE;
uint8_t _boot_record[512] = { 0 };
std::wstring _volume_name;
public:
explicit mt_reader(std::wstring volume_name);
~mt_reader();
mt_reader(const mt_reader&) = delete;
mt_reader& operator=(const mt_reader&) = delete;
mt_reader(mt_reader&&) = delete;
mt_reader& operator=(mt_reader&&) = delete;
// mt_reader(const mt_reader& reader2);
// mt_reader& operator=(const mt_reader& e);
HANDLE handle() const { return _handle_disk; };
std::expected<void, win_error> read(uint64_t offset, void *buf, uint32_t size);
};
#include <Windows.h>
#include <cstdint>
#include <expected>
#include <string>
#include <thread>
#include "mt_reader.hpp"
#include "Utils/error.hpp"
mt_reader::mt_reader(std::wstring volume_name) {
std::wstring valid_name = volume_name;
if (valid_name.back() == '\\') {
valid_name.pop_back();
}
_handle_disk = CreateFileW(
valid_name.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
nullptr
);
if (_handle_disk == INVALID_HANDLE_VALUE) {
auto err = win_error(GetLastError(),ERROR_LOC);
if (err.Get_First_Error_Code()!=0) {
win_error::print(err);
}
} else {
auto temp_read = read(0, _boot_record, 0x200, 'a');
if (!temp_read.has_value()) {
win_error::print(temp_read.error().add_to_error_stack("Caller: read error", ERROR_LOC));
}
}
}
mt_reader::~mt_reader() {
std::cout << "mt_reader closed" << std::endl;
if (_handle_disk != INVALID_HANDLE_VALUE) {
if (!CloseHandle(_handle_disk)) {
auto err = win_error(GetLastError(),ERROR_LOC);
if (err.Get_First_Error_Code()!=0) {
win_error::print(err);
}
}
} else {
std::cout << win_error("Invalid Handle Value", ERROR_LOC);
}
}
std::expected<void, win_error> mt_reader::read(uint64_t offset, void *buf, uint32_t size) {
OVERLAPPED ov{};
ov.Offset = DWORD(offset & 0xffffffff);
ov.OffsetHigh = DWORD(offset >> 32);
DWORD read = 0;
if (BOOL ok = ReadFile(
_handle_disk,
buf,
size,
nullptr,
&ov
); !ok) {
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING) {
return std::unexpected(win_error(err, ERROR_LOC));
}
ok = GetOverlappedResult(
_handle_disk,
&ov,
&read,
TRUE
);
if (!ok) {
return std::unexpected(win_error(GetLastError(), ERROR_LOC));
}
} else {
read = ov.InternalHigh;
}
if (read != size) {
return std::unexpected(win_error(ERROR_HANDLE_EOF, ERROR_LOC));
}
return {};
}
#include <iostream>
#include <vector>
#include <random>
#include <thread>
#include <atomic>
#include "Drive/mt_reader.hpp"
#include "Utils/error.hpp"
constexpr uint32_t SECTOR_SIZE = 0x200; // 512 bytes
constexpr uint32_t READ_SIZE = 0x200; // read 1 sector
constexpr int READ_LOOPS = 100;
uint64_t random_sector_offset(uint64_t max_sectors) {
static thread_local std::mt19937_64 rng{ std::random_device{}() };
std::uniform_int_distribution<uint64_t> dist(0, max_sectors - 1);
return dist(rng) * SECTOR_SIZE;
}
void reader_thread(mt_reader& reader, int thread_id) {
std::vector<std::uint8_t> buffer(READ_SIZE);
// Example limit: first 1 GB of disk
constexpr uint64_t MAX_SECTORS = (1ull << 30) / SECTOR_SIZE;
for (int i = 0; i < READ_LOOPS; ++i) {
uint64_t offset = random_sector_offset(MAX_SECTORS);
auto result = reader.read(offset, buffer.data(), READ_SIZE);
if (!result.has_value()) {
std::cerr << "[thread " << thread_id
<< "] read failed at offset 0x"
<< std::hex << offset << std::dec << "\n";
win_error::print(result.error());
} else {
std::cout << "[thread " << thread_id
<< "] read ok at offset 0x"
<< std::hex << offset << std::dec << "\n";
}
}
}
int main() {
// Example targets:
// L"\\\\.\\PhysicalDrive0"
// L"\\\\.\\C:"
mt_reader reader(L"\\\\.\\PhysicalDrive1");
std::vector<std::thread> threads;
threads.emplace_back(reader_thread, std::ref(reader), 0);
threads.emplace_back(reader_thread, std::ref(reader), 1);
threads.emplace_back(reader_thread, std::ref(reader), 2);
for (auto& t : threads) {
t.join();
}
return 0;
}
The problem I'm facing is with the ReadFile() API. I'm calling this function in individual threads to raw read from a disk. This works fine in a single thread, but the moment 2 or more threads execute this function I get Reached the end of the file., and the error code is 38.
win_error is a simple error propagation wrapper class, it just contains the error code and error string and location of the error in a stack.
You are using the OVERLAPPED incorrectly. You need to use an event object in its hEvent field. This is required if you want to have multiple threads reading from the same disk handle at the same time. Otherwise, completion status of a read will be stored in the disk handle itself, and multiple reads in parallel will step over each other's status, confusing GetOverlappedResult().
Per Microsoft's documentation:
Synchronization and Overlapped Input and Output
An event is needed only if there will be more than one outstanding I/O operation at the same time. If an event is not used, each completed I/O operation will signal the file, named pipe, or communications device.
If the
hEventmember of theOVERLAPPEDstructure is NULL, the system uses the state of thehFilehandle to signal when the operation has been completed. Use of file, named pipe, or communications-device handles for this purpose is discouraged. It is safer to use an event object because of the confusion that can occur when multiple simultaneous overlapped operations are performed on the same file, named pipe, or communications device. In this situation, there is no way to know which operation caused the object's state to be signaled.
Also, read != size does not mean EOF was reached. That is a mistake in your error handling. If the read is successful, it can return fewer bytes than requested. For a synchronous read, EOF is signaled by read == 0 when ReadFile() returns TRUE. But for an asynchronous read, EOF is signaled by GetOverlappedResult() failing with the ERROR_HANDLE_EOF error code. This is explained in the documentation: Testing for the End of a File. Either way, if you need the full size to be read then call ReadFile() in a loop until all requested bytes are actually read.
With that said, try this instead:
#include <Windows.h>
#include <cstdint>
#include <expected>
#include <string>
#include <thread>
#include "mt_reader.hpp"
#include "Utils/error.hpp"
mt_reader::mt_reader(std::wstring volume_name) {
if (volume_name.back() == L'\\') {
volume_name.pop_back();
}
_handle_disk = CreateFileW(
volume_name.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
nullptr
);
if (_handle_disk == INVALID_HANDLE_VALUE) {
auto err = win_error(GetLastError(),ERROR_LOC);
win_error::print(err);
} else {
auto temp_read = read(0, _boot_record, 0x200, 'a');
if (!temp_read.has_value()) {
win_error::print(temp_read.error().add_to_error_stack("Caller: read error", ERROR_LOC));
}
}
}
mt_reader::~mt_reader() {
std::cout << "mt_reader closed" << std::endl;
if (_handle_disk != INVALID_HANDLE_VALUE) {
if (!CloseHandle(_handle_disk)) {
auto err = win_error(GetLastError(),ERROR_LOC);
win_error::print(err);
}
} else {
std::cout << win_error("Invalid Handle Value", ERROR_LOC);
}
}
std::expected<void, win_error> mt_reader::read(uint64_t offset, void *buf, uint32_t size) {
OVERLAPPED ov{};
ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!ov.hEvent) {
return std::unexpected(win_error(GetLastError(), ERROR_LOC));
}
BYTE *pbuf = static_cast<BYTE*>(buf);
DWORD read;
while (size > 0) {
ov.Offset = static_cast<DWORD>(offset & 0xffffffff);
ov.OffsetHigh = static_cast<DWORD>(offset >> 32);
read = 0;
if (BOOL ok = ReadFile(
_handle_disk,
pbuf,
size,
nullptr,
&ov
); !ok) {
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING) {
CloseHandle(ov.hEvent);
return std::unexpected(win_error(err, ERROR_LOC));
}
ok = GetOverlappedResult(
_handle_disk,
&ov,
&read,
TRUE
);
if (!ok) {
err = GetLastError();
CloseHandle(ov.hEvent);
return std::unexpected(win_error(err, ERROR_LOC));
}
} else {
read = ov.InternalHigh;
}
if (read == 0) {
CloseHandle(ov.hEvent);
return std::unexpected(win_error(ERROR_HANDLE_EOF, ERROR_LOC));
}
offset += read;
size -= read;
pbuf += read;
}
CloseHandle(ov.hEvent);
return {};
}