11

I am implementing some file system in C++. Up to now I was using fstream but I realized that it is impossible to open it in exclusive mode. Since there are many threads I want to allow multiple reads, and when opening file in writing mode I want to open the file in exclusive mode?
What is the best way to do it? I think Boost offers some features. And is there any other possibility? I would also like to see simple example. If it is not easy / good to do in C++ I could write in C as well.

I am using Windows.

genpfault
  • 51,148
  • 11
  • 85
  • 139
rank1
  • 1,018
  • 4
  • 16
  • 37
  • 1
    Such low-level stuff is best done in C, with as little overhead as possible... – bash.d Apr 22 '13 at 06:51
  • 3
    @bash.d Nonsense, why would you think that? The whole point of C++ is to provide overhead-free abstractions. I had you pegged as somebody who knew that. – Konrad Rudolph Apr 22 '13 at 09:08
  • 1
    @KonradRudolph I don't know about you, but I don't know about many file-systems implemented in C++... – bash.d Apr 22 '13 at 09:10
  • @bash.d Grab the system functions, stuff them in a `std::streambuf` implementation. – Konrad Rudolph Apr 22 '13 at 09:11
  • @KonradRudolph Well, if it is no problem for you, provide a suitable answer! – bash.d Apr 22 '13 at 09:22
  • @KonradRudolph Finally, C++23 will provide such an abstraction - 12 years after C got the `x` mode for `fopen` and 6 years after C++ got the same `x` mode for `std::fopen`. Behold the new `std::ofstream` [`openmode`](https://stackoverflow.com/a/75017250/7582247) :-) – Ted Lyngmo Jan 05 '23 at 17:52

6 Answers6

5

On many operating systems, it's simply impossible, so C++ doesn't support it. You'll have to write your own streambuf. If the only platform you're worried about is Windows, you can possibly use the exclusive mode for opening that it offers. More likely, however, you would want to use some sort of file locking, which is more precise, and is available on most, if not all platforms (but not portably—you'll need LockFileEx under Windows, fcntl under Unix).

Under Posix, you could also use pthread_rwlock. Butenhof gives an implementation of this using classical mutex and condition variables, which are present in C++11, so you could actually implement a portable version (provided all of the readers and writers are in the same process—the Posix requests will work across process boundaries, but this is not true for the C++ threading primitives).

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • How and why would I write my own streambuf? I think it will be easier for me to just use this windows api as here http://msdn.microsoft.com/en-us/library/windows/desktop/aa365204(v=vs.85).aspx – rank1 Apr 22 '13 at 10:12
  • @SebastianCygert The Windows API requires outputting a `char` buffer; the `ostream` interface handles formatting and conversion to strings. The `streambuf` class is the bridge between the two. – James Kanze Apr 22 '13 at 11:11
3

if your app only works on Windows, the Win32 API function CreateFile() is your choice.

For example:

HANDLE hFile = ::CreateFileW(lpszFileFullPathName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
skyfree
  • 867
  • 2
  • 10
  • 29
2

If you are open to using boost, then I would suggest you use the file_lock class. This means you want to keep the filename of the files you open/close because fstream does not do so for you.

They have two modes lock() that you can use for writing (i.e. only one such lock at a time, the sharable lock prevents this lock too) and lock_sharable() that you can use for reading (i.e. any number of threads can obtain such a lock).

Note that you will find it eventually complicated to manage both, read and write, in this way. That is, if there is always someone to read, the sharable lock may never get released. In that case, the exclusive lock will never be given a chance to take....

// add the lock in your class
#include <boost/interprocess/sync/file_lock.hpp>
class my_files
{
...
private:
    ...
    boost::file_lock     m_lock;
};

Now when you want to access a file, you can lock it one way or the other. If the thread is in charge of when they do that, you could add functions for the user to have access to the lock. If your implementation of the read and write functions in my_files are in charge, you want to get a stack based object that locks and unlocks for you (RAII):

class safe_exclusive_lock
{
public:
    safe_exclusive_lock(file_lock & lock)
        : m_lock_ref(lock)
    {
        m_lock_ref.lock();
    }
    ~safe_exclusive_lock()
    {
        m_lock_ref.unlock();
    }
private:
    file_lock & m_lock_ref;
};

Now you can safely lock the file (i.e. you lock, do things that may throw, you always unlock before exiting your current {}-block):

ssize_t my_files::read(char *buf, size_t len)
{
    safe_exclusive_lock guard(m_lock);
    ...your read code here...
    return len;
} // <- here we get the unlock()

ssize_t my_files::write(char const *buf, size_t len)
{
    safe_exclusive_lock guard(m_lock);
    ...your write code here...
    return len;
} // <- here we get the unlock()

The file_lock uses a file, so you will want to have the fstream file already created whenever the file_lock is created. If the fstream file may not be created in your constructor, you probably will want to transform the m_lock variable in a unique pointer:

private:
    std::unique_ptr<file_lock>  m_lock;

And when you reference it, you now need an asterisk:

safe_exclusive_lock guard(*m_lock);

Note that for safety, you should check whether the pointer is indeed allocated, if not defined, it means the file is not open yet so I would suggest you throw:

if(m_lock)
{
    safe_exclusive_lock guard(*m_lock);
    ...do work here...
}
else
{
    throw file_not_open();
}
// here the lock was released so you cannot touch the file anymore

In the open, you create the lock:

bool open(std::string const & filename)
{
    m_stream.open(...);
    ...make sure it worked...
    m_lock.reset(new file_lock(filename));
    // TODO: you may want a try/catch around the m_lock and
    //       close the m_stream if it fails or use a local
    //       variable and swap() on success...
    return true;
}

And do not forget to release the lock object in your close:

void close()
{
    m_lock.reset();
}
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
  • Related [boost interprocess_lock does not work with multiple processes](http://stackoverflow.com/questions/6697704/boost-interprocess-file-lock-does-not-work-with-multiple-processes) – Alexis Wilke Jun 01 '16 at 03:31
0

Well you can manually prevent yourself from opening a file if it has been opened in write mode already. Just keep track internally of which files you've opened in write mode.

Perhaps you could hash the filename and store it in a table upon open with write access. This would allow fast lookup to see if a file has been opened or not.

RandyGaul
  • 1,915
  • 14
  • 21
  • I am not sure if it is the best idea because I will have to keep all the open files. (Note that if one fileA is open in read mode, and someone wants to open it in write mode it would need to wait until the read mode is done). – rank1 Apr 22 '13 at 07:38
0

You could rename the file, update it under the new name, and rename it back. I've done it, but it's a little heavy.

user3029478
  • 179
  • 1
  • 2
0

Since C++17 there are two options:

  • In C++23 by using the openmode std::ios::noreplace.
  • In C++17 by using the std::fopen mode x (exclusive).
    Note: The x mode was added to in C11.

C++23 and later:

#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>

int main() {
    std::ofstream ofs("the_file", std::ios::noreplace);

    if (ofs) {
        std::cout << "success\n";
    } else {
        std::cerr << "Error: " << std::strerror(errno) << '\n';
    }
}

Demo


C++17 and later:

#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>

struct FILE_closer {
    void operator()(std::FILE* fp) const { std::fclose(fp); }
};

// you may want overloads for `std::filesystem::path`, `std::string` etc too:
std::ofstream open_exclusively(const char* filename) {
    bool excl = [filename] {
        std::unique_ptr<std::FILE, FILE_closer> fp(std::fopen(filename, "wx"));
        return !!fp;
    }();
    auto saveerr = errno;

    std::ofstream stream;
    
    if (excl) {
        stream.open(filename);
    } else {
        stream.setstate(std::ios::failbit);
        errno = saveerr;
    } 
    return stream;
}
int main() {
    std::ofstream ofs = open_exclusively("the_file");

    if (ofs) {
        std::cout << "success\n";
    } else {
        std::cout << "Error: " << std::strerror(errno) << '\n';
    }
}

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108