Hah, I think I know what’s happening! The problem is that recv() first checks whether the UDP socket is “active” and only if it’s found to be not active then it registers a callback for when a matching UDP datagram arrives. From Sockets.jl:
if ccall(:uv_is_active, Cint, (Ptr{Cvoid},), sock.handle) == 0
err = ccall(:uv_udp_recv_start, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
sock,
@cfunction(Base.uv_alloc_buf, Cvoid, (Ptr{Cvoid}, Csize_t, Ptr{Cvoid})),
@cfunction(uv_recvcb, Cvoid, (Ptr{Cvoid}, Cssize_t, Ptr{Cvoid}, Ptr{Cvoid}, Cuint)))
The trouble is that sending a datagram also counts as active; therefore, if the recv() is executed during the send() then the callback is never registered and therefore the recv() never resolves.
julia> sock = UDPSocket()
bind(sock, ip"127.0.0.1", 0)
println("Before send: ", is_active(sock))
@async begin
println("In async before yield: ", is_active(sock))
println("In async after yield: ", is_active(sock))
end
send(sock, ip"127.0.0.1", 9809, "hello")
println("After send: ", is_active(sock))
close(sock)
Before send: 0
In async before yield: 1
In async after yield: 0
After send: 0
This also explains why delaying the recv() even further by putting a sleep() before it fixes the problem: the extra sleep means the task yields once more, and that allows the send() to finish before the recv() starts.