1

I am setting up a (receiving) thread that uses std::getline on a fifo (named pipe). The peer may or may not be present (actually I do not get to the getline until I know there is a peer) and may or may not send data. Eventually, I may decide to shutdown the operation of the FIFO.

I need to be able to cleanup the receiving thread. But for this std::getline must return. I was hoping that doing a close on the std::ifstream would suffice. But it appears not to be the case.

I have this minimal case that hangs at the line rr.get()

  void test() {
    std::filesystem::path              path1("/tmp/f1");
    int                                retval = mkfifo(path1.string().c_str(), (unsigned int)0660);

    // Open TX side
    std::ofstream                      txStream;

    std::future<void>                  tt = std::async([&]() {
      txStream.open(path1, std::ios::binary | std::ios::out | std::ios::trunc);
      return;
    });

    // Open RX side
    std::ifstream                      rxStream;
    rxStream.open(path1, std::ios::binary | std::ios::in);

    // Setup getline
    std::future<void>                  rr = std::async([&]() {
      std::string                      line = "XX";
      std::getline(rxStream, line);
      std::cout << " Line is: " << line << std::endl;
    });

    std::this_thread::sleep_for(250ms);
    rxStream.close();

    if (tt.valid() == true) {
      tt.get();
      std::cout << "Done Tx" << std::endl;
    } else {
      std::cout << "Tx task not valid" << std::endl;
    }

    if (rr.valid() == true) {
      rr.get();
      std::cout << "Done Rx" << std::endl;
    } else {
      std::cout << "Rx task not valid" << std::endl;
    }
  }

Is there an alternative and reliable way to unblock std::getline?

Claude
  • 51
  • 2
  • 7
  • 1
    Are you sure that you are blocked at `getline()` not at `rxStream.open()` ? – Slava Jan 20 '23 at 15:03
  • Using gdb, I see that the Rx thread is stuck at getline. The rxStream.open() completes because of the Tx thread (fake) that opens the fifo (other side) – Claude Jan 20 '23 at 15:58

2 Answers2

0

You have a data race in your program because

  1. C++ iostreams are not thread-safe.
    You can't close a stream while another thread is doing a read.

  2. POSIX file I/O is not thread-safe.
    You can't close a file descriptor while another thread is doing a read. Even if the Linux kernel is lenient about this, a blocking FIFO read can not be stopped from the RX side.

Rethink your design. FIFO's are thread-safe. Close the TX side of the FIFO, and the RX side will return EOF.

If you can't control the TX side at all, the best you can try is reading in non-blocking mode, this is outside the capabilities of C++ I/O streams.


Bonus notes:

  • you don't need to check tt.valid(), it is valid since you got it from std::async.
  • build with -fsanitize==thread to enable ThreadSanitizer. It'll tell you if you have a data race.
rustyx
  • 80,671
  • 25
  • 200
  • 267
  • Well, the test case is just that, a test case. As I said at the beginning of my post, the TX side (which the peer in another process) may or may not exists. In other word, real life scenario is that the FIFO may never receive anything, which why the test case does not send anything in the FIFO. But opens it to simulate that the peer exists but is quiet. – Claude Jan 20 '23 at 16:13
  • The tt.valid() is just a check; in a simple piece of code you are correct it is not necessary. But real life code will need it if the future is kept in some structure elsewhere. Doing a get on future that is not valid is not defined by the standard. – Claude Jan 20 '23 at 16:16
  • In the first link (the one about C++ iostreams), I am not sure the answers given understood that the OP is using different ifstream in each thread; I think this is Ok since it is like opening the same file multiple times. Of course the data may be mangled but that is another story. – Claude Jan 20 '23 at 16:18
  • The 2nd link (POSIX file IO) is interesting. Ultimately, this means that you can not use getline with fifo, since, by definition, everything can happen (no peer, quiet peer, etc.) and you get stuck with no way out. You could poll or use select, but then you should still not use getline since the data may not be complete (the peer might have crashed or wrote an incomplete line, etc.). The only option I see is ifstream::readsome, do the line separation myself and rely on select or poll – Claude Jan 20 '23 at 16:28
  • You can't abort a blocking FIFO read from another thread. I've updated the answer to make that bit clear. – rustyx Jan 20 '23 at 16:32
  • Yeap. But if I read readsome correctly, it should do the trick. Coupling it with some select or polling mechanism (to know there is some, albeit potentially incomplete, data) + having a closing mechanism for the stream in the same thread. And forget getline completely. – Claude Jan 20 '23 at 16:48
  • I think in this case you better use a socket instead of pipe either directly or through some library. – Slava Jan 20 '23 at 17:22
0

With the help from the answer provided by @rustyx and @slava I understand that std::fstream::getline is not a solution if you want to be able to cleanup regardless of the outcome of using the fifo (no peer, crashed peer, etc.).

So, the solution is decided to implement is to simply poll the fifo with std::fstream::rdbuf()::in_avail() and then use std::fstream::readsome(). Of course, this means that line delineation must be performed manually after reading. The thread terminate at each polling cycle so it is a rather expensive solution but ok for my use case.

Other solutions exist, including switching to a socket alternative.

Claude
  • 51
  • 2
  • 7