Nim: asyncfile.readline blocks (and behaves differently between Linux and MacOS)

Created on 22 Jun 2019  路  7Comments  路  Source: nim-lang/Nim

We had a bug on MacOS and realized that in the following code the second readline blocks whereas it doesn't block on Linux.

import asyncdispatch, asyncfile                                                           

proc myTask() {.async.} =                                                                                                        
  var inputFile = openAsync("/dev/stdin", fmRead)                                              
  var inputFuture = inputFile.readline()                                                  
  while true:                                                                             
    let input = await inputFuture                                                         
    echo "Got input: ", input                                                              
    echo "The next call should never block"                                               
    inputFuture = inputFile.readLine()                                                    
    echo "This message should show instantly"                                             

proc main() =                                                                             
  asyncCheck myTask()                                                                     
  runForever()                                                                            

main()

Output on Linux:

$ ./test
hallo
Got input: hallo
The next call should never block
This message should show instantly
welt
Got input: welt
The next call should never block
This message should show instantly

Output on MacOS:

$ ./test 
hallo
Got input: hallo
The next call should never block
welt
This message should show instantly
Got input: welt
The next call should never block

If we replace openAsync("/dev/stdin", fmRead) with newAsyncFile(AsyncFD(0)) it blocks on Linux, too.

I expected that readline would never block and returns a Future immediately.

Nim version: 0.20.0

If there is a better way to read stdin asynchronously, I would be really interested.

Async

Most helpful comment

@jeremija
Yeah, same issue going on. All of the asyncfile apis have this problem until we get multithreaded async working. (Something I'm actively working on)

until then, you can use a similar work around. This should give the expected output:
(Note: it uses threads, so you need to compile with --threads:on)

import asyncdispatch, asyncfile, threadpool                                                                                                                                                                        

proc asyncReadAll(path: string): Future[string] =                                                                                                                                                                  
  let event = newAsyncEvent()                                                                                                                                                                                      
  let future = newFuture[string]("asyncReadline")                                                                                                                                                                  
  proc readlineBackground(path: string, event: AsyncEvent): string =                                                                                                                                               
    try:                                                                                                                                                                                                           
      result = cast[string](readFile(path))                                                                                                                                                                        
    except:                                                                                                                                                                                                        
      # unfortunately we don't have a good way to get exceptions accross thread boundaries                                                                                                                         
      result = getCurrentExceptionMsg()                                                                                                                                                                            
    finally:                                                                                                                                                                                                       
      event.trigger()                                                                                                                                                                                              

  let flowVar = spawn readlineBackground(path, event)                                                                                                                                                              
  proc callback(fd: AsyncFD): bool =                                                                                                                                                                               
    future.complete(^flowVar)                                                                                                                                                                                      
    true                                                                                                                                                                                                           
  addEvent(event, callback)                                                                                                                                                                                        
  return future                                                                                                                                                                                                    

proc readFiles(): Future[string] {.async.} =                                                                                                                                                                       
  echo "start reading"                                                                                                                                                                                             
  let res = await asyncReadAll("numbers.txt")                                                                                                                                                                      
  echo "end reading"                                                                                                                                                                                               
  return res                                                                                                                                                                                                       

echo "call readFiles()"                                                                                                                                                                                            
let f = readFiles()                                                                                                                                                                                                
echo "waiting for result..."                                                                                                                                                                                       
let res = waitFor f                                                                                                                                                                                                
echo "done"  

All 7 comments

Yeah, you cannot read stdin asynchronously like this.

You have to use a little bit of a workaround, currently it looks like this: https://github.com/dom96/nim-in-action-code/blob/master/Chapter3/ChatApp/src/client.nim#L42-L50. Eventually once awaiting a spawned proc becomes possible it'll be much easier (and far more efficient) (CC @rayman22201)

Thanks for the workaround, unfortunately it didn't work for me in combination with other async tasks.

But it helped me develop an async readline function:

proc asyncReadline(): Future[string] =                                                    
  let event = newAsyncEvent()                                                             
  let future = newFuture[string]("asyncReadline")                                         
  proc readlineBackground(event: AsyncEvent): string =                                    
    result = stdin.readline()                                                             
    event.trigger()                                                                       
  let flowVar = spawn readlineBackground(event)                                           
  proc callback(fd: AsyncFD): bool =                                                      
    future.complete(^flowVar)                                                             
    true                                                                                 
  addEvent(event, callback)                                                               
  return future

That's actually a very nice way to implement this. When I implemented the code above we didn't have async events. This seems like it could be a relatively easy way to implement await support for FlowVar's (the return type of spawned procs).

What do you think @rayman22201 ?

It's a clean way to implement this.

AsyncEvent seems to make adding custom event loop triggers much easier.
I think they are efficient-ish?

I could definitely see extending this to the general case of async spawn.

It also illustrates the reason why I think flowVars are superfluous....
The code is essentially a wrapper for a thread future (flowVar) inside a regular future.
Why can't spawn just do this internally and return a normal Future directly?

I have a similar issue when using asyncfile.readAll() (and readLine too), but I'm reading an actual file. Here is my example:

import asyncdispatch, asyncfile

proc readFiles(): Future[string] {.async.} =
  var file = openAsync("numbers.txt", fmRead)
  try:
    echo "start reading"
    let res = await file.readAll()
    echo "end reading"
    return res
  finally:
    file.close()

echo "call readFiles()"
let f = readFiles()
echo "waiting for result..."
let res = waitFor f
echo "done"

The numbers.txt file was generated with seq 1 10000 > numbers.txt. I've tried changing the file size and did not seem to make a difference.

nim c -r example.vim
# or docker
docker run -it --rm -v $PWD/numbers.txt:/numbers.txt -v $PWD/example.nim:/example.nim nimlang/nim:latest nim c -r /example.nim

I get the following output on both Mac OS and Linux:

call readFiles()
start reading
end reading
waiting for result...
done

Coming from NodeJS, I expected the output to be:

call readFiles()
start reading
waiting for result...
end reading
done

Is there something I'm missing?

@jeremija
Yeah, same issue going on. All of the asyncfile apis have this problem until we get multithreaded async working. (Something I'm actively working on)

until then, you can use a similar work around. This should give the expected output:
(Note: it uses threads, so you need to compile with --threads:on)

import asyncdispatch, asyncfile, threadpool                                                                                                                                                                        

proc asyncReadAll(path: string): Future[string] =                                                                                                                                                                  
  let event = newAsyncEvent()                                                                                                                                                                                      
  let future = newFuture[string]("asyncReadline")                                                                                                                                                                  
  proc readlineBackground(path: string, event: AsyncEvent): string =                                                                                                                                               
    try:                                                                                                                                                                                                           
      result = cast[string](readFile(path))                                                                                                                                                                        
    except:                                                                                                                                                                                                        
      # unfortunately we don't have a good way to get exceptions accross thread boundaries                                                                                                                         
      result = getCurrentExceptionMsg()                                                                                                                                                                            
    finally:                                                                                                                                                                                                       
      event.trigger()                                                                                                                                                                                              

  let flowVar = spawn readlineBackground(path, event)                                                                                                                                                              
  proc callback(fd: AsyncFD): bool =                                                                                                                                                                               
    future.complete(^flowVar)                                                                                                                                                                                      
    true                                                                                                                                                                                                           
  addEvent(event, callback)                                                                                                                                                                                        
  return future                                                                                                                                                                                                    

proc readFiles(): Future[string] {.async.} =                                                                                                                                                                       
  echo "start reading"                                                                                                                                                                                             
  let res = await asyncReadAll("numbers.txt")                                                                                                                                                                      
  echo "end reading"                                                                                                                                                                                               
  return res                                                                                                                                                                                                       

echo "call readFiles()"                                                                                                                                                                                            
let f = readFiles()                                                                                                                                                                                                
echo "waiting for result..."                                                                                                                                                                                       
let res = waitFor f                                                                                                                                                                                                
echo "done"  

I'm seeing this same behavior in v1.2.4. This means we don't have real non-blocking file IO with async/await. It seems the documentation should be updated to explain this. I was expecting behavior similar to node.js as well.

Was this page helpful?
0 / 5 - 0 ratings