Non-blocking network IO

I think I may have figured it out:

julia> c1=accept(svr)
TCPSocket(RawFD(26) open, 0 bytes waiting)

julia> bytesavailable(c1)
0
# at this point I typed into netcat
julia> bytesavailable(c1)
0

julia> Base.start_reading(c1) # in stream.jl
0

julia> bytesavailable(c1)
5

julia> Base.start_reading(c1)
0

julia> read(c1,5)
5-element Vector{UInt8}:
 0x61
 0x6f
 0x65
 0x75
 0x0a
# typed some more
julia> bytesavailable(c1)
5

julia> read(c1,5)
5-element Vector{UInt8}:
 0x73
 0x6e
 0x74
 0x68
 0x0a

julia> bytesavailable(c1)
0
# typed some more
julia> bytesavailable(c1)
5

Does start_reading return immediately? There’s also stop_reading. I typed something, then tried Base.start_reading(c1);Base.stop_reading(c1), but no bytes were available. Then I did Base.start_reading(c1);sleep(0.001);Base.stop_reading(c1) and got all seven bytes available.

You mean that if I run Julia and run this networking program in my Julia process, and someone else runs another Julia and sends a gigabyte, he could block my Julia?

In this Julia program, there won’t be another library transferring gigabytes at once.

No. I mean that if in your process, some other julia Task object sends (maybe even receives!) a large amount of data in one read/write call, it could result in your program having to wait for that operation to complete, due to that process-global shared lock.

I’m not taking about multiprocessing - that would not share ressources/locks between different julia instances anyway.

Ok, having run the experiment I mentioned above now, I have to rescind the claim that this iolock_begin() will cause other tasks to wait. I couldn’t observe this even with julia -t 1 and having external ncat doing the processing/shoveling of data from /dev/urandom, with the julia Tasks read/writeing the result. Maybe my methodology is wrong, or I misunderstood the code/purpose of that lock :thinking:

As was true in 2017 from the link in OP’s post, and still remains true today, Julia uses non-blocking I/O for all streams, since Julia is green threaded (uses Tasks instead of threads):

@phma the answers to all of your questions will be the same: use a task in some form (@async or Threads.@spawn are the most common) and write synchronous-looking code. It will automatically be converted into a non-blocking form internally (through the benefit of Tasks). When you are done with the socket (e.g. because it was told to shutdown) call close on all sockets that you want to destroy from any task.

1 Like

I’d say that the exposed API is, by its nature, still blocking. The lowest form of parallel construct that Julia exposes is being blocked from progressing after all; asynchronous I/O would IMO imply that this is not the case, and that the function returns immediately, without having to spawn a Task by hand.

Sure, if you wrap every blocking call into a thunk that you then manually spawn a concurrent primitive for, you end up with a “non-blocking” API, but by that argument, using pthread_spawn around write in C also makes POSIX natively asynchronous (albeit horribly racy due to the lack of locks behind the scenes), even without aio_read/aio_write. This is of course nonsense - write is still a blocking call, otherwise there wouldn’t be a need for aio_read/aio_write or io_uring in the first place.

1 Like