syscall.Read() with timeout: syscall.Select() vs golang's select statement

2,189 views
Skip to first unread message

fabian....@gmail.com

unread,
Jul 24, 2016, 6:50:37 PM7/24/16
to golang-nuts
Hi,

I would like to call syscall.Read() with a timeout. Coming from C, my first idea was to use syscall.Select(), which should work fine. However, I came across this answer from 2014 by Ian Lance Taylor to a similar question (https://groups.google.com/d/msg/golang-nuts/al-6_QFgYaM/UkCqyCQVq_0J):

The usual approach in Go is to use a simple goroutine that reads from the file and writes to a channel.  Then you can use the select statement to check the channel when appropriate.  It's unusual to want to use the poll or select system calls since the language gives you other ways to do the same thing.

I tried it, but I find it harder than it sounds. Here's my pseudo code:

data := make(chan byte)
fd
:= some file descriptor

go func
() {
    buf
= make([]byte, 1)
   
for {
        n
, err = syscall.Read(fd, buf)
       
if err != nil {
            close
(data)
           
return
       
}
        data
<- buf[0]
   
}
}

select {
   
case b := <- data:
       
// do something with b
   
case <- timeout:
        syscall
.Close(fd)
}

The idea is obvious: When the timeout is reached, fd is closed, which interrupts the blocking syscall.Read(), which terminates the goroutine.

However, there is a race condition: If the timeout is reached immediately after a successful syscall.Read(), the line 'data <- buf[0]' will block forever, as no consumer reads data anymore. Adding capacity to the data channel doesn't help, because theoretically the loop might repeat multiple times between the timeout is received and fd is actually closed.

How can I refactor this pseudo code to get rid of the race condition? How can I implement a syscall.Read() with timeout using a goroutine, channel, and golang's select statement?

Thanks for your help
Fabian

Alex Bucataru

unread,
Jul 24, 2016, 11:12:48 PM7/24/16
to golang-nuts, fabian....@gmail.com
Hi Fabian,

You could add a signal channel to tell your goroutine to exit. Editing your pseudo code:

data := make(chan byte)
done := make(chan struct{})
fd
:= some file descriptor

go func
() {

    buf
= make([]byte, 1)
   
for {
        n
, err = syscall.Read(fd, buf)
       
if err != nil {
            close
(data)
           
return
       
}

        select {
            case data <- buf[0]:
            case <- done:
               
return
       
}
    }
}

select {
   
case b := <- data:
       
// do something with b
   
case <- timeout:
        close(done)
        syscall
.Close(fd)
}

Hope this helps.

Cheers,
Alex

fabian....@gmail.com

unread,
Jul 25, 2016, 8:49:30 AM7/25/16
to golang-nuts, fabian....@gmail.com
Nice! I wasn't aware that the select statement is also for writing to channels, most examples only show reading from channels. Thanks a lot!

Sam Vilain

unread,
Jul 26, 2016, 4:09:13 PM7/26/16
to golan...@googlegroups.com
On 7/24/16 12:49 PM, fabian....@gmail.com wrote:
> The idea is obvious: When the timeout is reached, fd is closed, which
> interrupts the blocking syscall.Read(), which terminates the goroutine.

I'm wondering how to do this safely for filehandles I don't want to
close, for instance stdin or stdout. It seems that reads and writes to
filehandles can't be interrupted or canceled. If you have a deadline
there's no way to guarantee that you don't read or write anything after
the deadline expires. This seems odd given that the underlying select
or epoll system calls are definitely capable of this behavior.

Overall it seems like a gap that I can't do file IO operations via
channels. It means that I can't pass around io.Reader and io.Writer for
temporary functions: they must always be read from a routine which will
own them until they are closed. Should I wrap my readers and writers in
these goroutines, and require my callers deal in some kind of
channel-based IO API that I invent? I appreciate that the current API
avoids an allocation and this can be important for many use cases, and a
read channel would have to be chan []byte or something and this would do
a lot more allocations, but doing so would at least make the behavior
more consistent and controllable. Maybe there could be a channel I
could wait on which translates to the filehandle ready bits, which lets
me know that I can make an read or a write which will operate without
blocking?

The answer I got when I asked this in the #general channel yesterday was
that the reader needs to implement its own, independent timeout. But
the regular object returned by os.OpenFile doesn't, and I'm struggling
to see what primitives this can be implemented with.

Sam

Matt Harden

unread,
Jul 26, 2016, 7:06:51 PM7/26/16
to Sam Vilain, golan...@googlegroups.com
Select and epool actually aren't useful for fd's that are opened on regular files. As I understand it, they always indicate those files are ready for I/O. You may be able to get something working using io_setup and friends on Linux: http://man7.org/linux/man-pages/man2/io_setup.2.html, but that would not be cross-platform. I think select etc. do work for pipes, and the Go stdlib doesn't have a way to implement timeouts on those currently. What kind of files do you want to do this for?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ishwor Gurung

unread,
Jul 26, 2016, 9:05:21 PM7/26/16
to golan...@googlegroups.com
On 27/07/2016 09:05, Matt Harden wrote:
> Select and epool actually aren't useful for fd's that are opened on regular
> files. As I understand it, they always indicate those files are ready for

This is not entirely accurate. The select(2) and epoll(7) _can_ be used
on a regular file descriptors; e.g. on platforms that do not support
inotify(7) (OpenBSD). They are generally part of the readyness I/O model
available on linux and as such provide a way for any registered parties
to watch for `events' on file/socket descriptors.

[...]

--
Best regards,
Ishwor Gurung

Sam Vilain

unread,
Jul 26, 2016, 11:37:04 PM7/26/16
to Matt Harden, golan...@googlegroups.com
On 7/26/16 4:05 PM, Matt Harden wrote:
> Select and epool actually aren't useful for fd's that are opened on
> regular files. As I understand it, they always indicate those files
> are ready for I/O.

That may well be the case for regular files. I'm talking in particular
about filehandles which are connected to a TTY, which is a character
device. Pipes (another strong possibility for standard input/output)
are also not regular files. In general it's up to the operating system
what kind of file it is you get when you call open(2), or what any file
descriptor you receive on process start-up is connected to.

As a thought experiment I half mocked up a module for this called
chanio; some highlights:

// Reader declares extra methods over an io.Reader which allow data to
// be read from channels and not just via the Read() method.
type Reader interface {
io.Reader
ReadChan() <-chan []byte
ReadError() <-chan error
}

// Writer declares extra methods over an io.Writer which allow data to
// be read from channels and not just via the Writer() method.
type Writer interface {
io.Writer
WriteChan() chan<- []byte
WriteError() <-chan error
}

// WrappedReader wraps a regular io.Reader and satisfies the Reader
// (and ReadCloser) interface
type WrappedReader struct {
rBuf []byte
blockSz int
readErr chan error
R io.Reader
rChan chan []byte
}

func (wr *WrappedReader) wrapRead(r io.Reader, chanSz, blockSz int) {
wr.R = r
wr.blockSz = blockSz
wr.buf = nil
wr.rChan = make(chan []byte, chanSz)
wr.readErr = make(chan error)
go wr.readLoop()
}

func (wr *WrappedReader) readLoop() {
var n int
var err error
for {
block := make([]byte, wr.blockSz)
n, err = wr.R.Read(block)
if err != nil {
wr.readErr <- err
// TODO: some errors are expected eg EINTR
close(wr.rChan)
return
}
if n != 0 {
wr.rChan <- block[:n]
}
}
}

// Read allows a channel IO object to be used wherever a normal Reader
is used
func (wr *WrappedReader) Read(buf []byte) (int, error) {
// buffered read
if len(wr.rBuf) > len(buf) {
buf[0:len(buf)] = wr.rBuf[0:len(buf)]
wr.rBuf = wr.rBuf[len(buf):]
return len(buf), nil
} else if len(wr.rBuf) > 0 {
// partial read
n := len(wr.rBuf)
buf[0:n] = wr.rBuf
wr.rBuf = nil
return n, nil
}
// no data ready - read from the channel
select {
case block := <-wr.ReadChan():
wr.rBuf = append(wr.rBuf, block)
return wr.Read(buf)
case err := <-wr.ReadError():
return -1, err
}
}

func WrapReader(reader io.Reader) Reader {
wr := &WrappedReader{}
wr.wrapRead(reader, 1, 1024)
return wr
}

func (wr *WrappedReader) ReadChan() <-chan []byte {
return wr.rChan
}

func (wr *WrappedReader) ReadError() <-chan error {
return wr.readErr
}

This might be a bit less awkward as a common API than having to use Read
everywhere. Clearly it would be better if there was a way to create one
of these chanio.Reader objects from something with a native select(2)
backing for channel reads, which could be a different constructor.

Thoughts?
Sam
Reply all
Reply to author
Forward
0 new messages